ASP.NET MVC 应用程序中的自定义基于角色的访问控制 (RBAC) - 第二部分 (基于角色的报告)






4.90/5 (32投票s)
使用 Entity Framework 为 ASP.NET MVC 应用程序介绍自定义基于角色的报告。保护机密信息是业务需求,在许多情况下也是道德和法律要求。
引言
在本篇文章中,我将扩展我们在 第一部分 中定义的自定义基于角色的访问控制 (RBAC) 框架,以包含动态的基于角色的报告。基于角色的访问控制 (RBAC) 是一种根据用户在组织中的角色来调节对应用程序功能或资源的访问的方法。角色的定义根据组织中的工作要求、权限和责任。然而,实施 RBAC 也带来挑战。确定每个角色将被分配的权限需要时间和精力,但也有巨大的好处。系统管理的负担可以大大减轻;随着组织需求的发展,可以轻松创建、更改或取消角色,而无需单独更新每个用户的权限。基于角色的访问控制 (RBAC) 现在已被大多数大型组织广泛接受为设置此类控件的最佳实践。
扩展的框架将提供功能,使应用程序系统管理员能够定义应用程序报告,并将每个报告与用户角色关联,从而控制哪些用户可以访问哪些报告。报告定义过程仅需要报告名称/描述、数据库存储过程名称和报告筛选器参数(可选)。用于向最终用户呈现报告结果的默认报告结果模板(cshtml)可以替换为您自定义设计的模板。报告定义的全过程,包括数据库存储过程的创建,可以委托给应用程序开发人员以外的人员。报告定义可以委托给应用程序系统管理员,数据库存储过程的创建可以委托给数据库管理员。扩展的框架将根据用户的角色显示报告列表;然后可以通过用户界面屏幕显示每个报告,并可以选择在显示报告结果之前进行筛选。
背景
无论何时,为任何组织开发应用程序都需要一个报告功能(无论是屏幕还是打印机),以便报告应用程序数据库中存储的数据。在大多数组织中,并非所有用户都被允许访问系统内定义的每份报告是很常见的。总的来说,通常为每个角色允许访问一部分报告,从而控制哪些用户可以访问哪些报告,而用户可以与多个角色关联。我们的报告框架扩展是特意设计的,非常灵活,使我们的默认报告解决方案可以替换为其他供应商的解决方案,而不会影响底层的 RBAC 报告功能。为了演示框架的灵活性,我们将替换我们的默认报告解决方案以集成 Microsoft 的 SQL Server 报告服务 (SSRS) 解决方案。
无论哪种情况,无论底层报告解决方案如何,我们的报告都将由应用程序报告定义中定义的报告参数驱动。
扩展数据库
为了扩展我们的框架以包含动态的基于角色的报告,我们需要在原始 RBAC 数据库中添加一个新的报告表和相应的链接表。我们更新后的实体关系 (ER) 图和数据库架构图如下所示,新添加的报告实体已用黄色高亮显示。
RBAC 实体关系图
RBAC 数据库架构图
我们 RBAC 数据模型中的 ROLES 表是基于角色的访问的关键,所有“自定义”实体都可以与之关联。这使得通过简单地将新表添加到与 Roles 表关联的 RBAC 数据模型(如上图所示)并将其所需功能暴露在我们的 RBACUser
类中,就可以生成由基于角色的访问驱动的功能。
RBAC SQL 架构更新脚本
可下载的示例项目包含两个 SQL 脚本文件,名为 'RBAC_FullSchema.sql' 和 'RBAC_UpdateSchema.sql'。前者脚本创建一个新的 RBAC 数据库架构,而后者脚本更新一个现有的 RBAC 数据库架构(在第一部分中创建)以仅包含新的报告表。
每个脚本都将创建 REPORTS、PARAMETERS、LNK_ROLE_REPORT 和 LNK_REPORT_PARAMETER 表,以及所有必要的表约束(表约束和链接表未在以下 SQL 脚本列表中显示)。
--Create REPORTS table...
CREATE TABLE [dbo].[REPORTS](
[Report_Id] [int] IDENTITY(1,1) NOT NULL,
[LastModified] [datetime] NOT NULL,
[Inactive] [bit] NOT NULL,
[ReportName] [nvarchar](150) NOT NULL,
[ReportDescription] [nvarchar](max) NULL,
[Template] [nvarchar](50) NULL,
[StoredProcedureName] [nvarchar](50) NULL,
CONSTRAINT [PK_REPORTS] PRIMARY KEY CLUSTERED
(
[Report_Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
...
--Create PARAMETERS table...
CREATE TABLE [dbo].[PARAMETERS](
[Parameter_Id] [int] IDENTITY(1,1) NOT NULL,
[ParameterName] [nvarchar](30) NOT NULL,
[ParameterType] [nvarchar](50) NULL,
[DisplayLabel] [nvarchar](50) NULL,
[Required] [bit] NULL
CONSTRAINT [PK_PARAMETERS] PRIMARY KEY CLUSTERED
(
[Parameter_Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
扩展我们的 Entity Framework (EF) RBAC 模型
现在我们已经更新了 SQL 数据库架构,我们需要扩展我们的 Entity Framework (EF) 数据库上下文模型以反映新的 REPORTS、PARAMETERS 和关联的链接表。代码中用灰色高亮显示的节(参见本文档的 pdf 版本)说明了在扩展原始框架以实现 RBAC 报告时所需的附加代码。如果您正在将 RBAC 实现为一个新项目,或者集成到一个尚未使用 RBAC 的现有项目中,请忽略高亮显示的节,并参考可下载示例项目中的整个框架。
using System;
using System.Data.Entity;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
public partial class RBAC_Model : DbContext
{
public RBAC_Model()
: base("name=RBAC_Model")
{
}
public virtual DbSet<PERMISSION> PERMISSIONS { get; set; }
public virtual DbSet<ROLE> ROLES { get; set; }
public virtual DbSet<USER> USERS { get; set; }
public virtual DbSet<REPORT> REPORTS { get; set; }
public virtual DbSet<PARAMETER> PARAMETERS { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<PERMISSION>()
.HasMany(e => e.ROLES)
.WithMany(e => e.PERMISSIONS)
.Map(m => m.ToTable("LNK_ROLE_PERMISSION").MapLeftKey("Permission_Id").MapRightKey("Role_Id"));
modelBuilder.Entity<ROLE>()
.HasMany(e => e.USERS)
.WithMany(e => e.ROLES)
.Map(m => m.ToTable("LNK_USER_ROLE").MapLeftKey("Role_Id").MapRightKey("User_Id"));
modelBuilder.Entity<REPORT>()
.HasMany(e => e.ROLES)
.WithMany(e => e.REPORTS)
.Map(m => m.ToTable("LNK_ROLE_REPORT").MapLeftKey("Report_Id").MapRightKey("Role_Id"));
modelBuilder.Entity<PARAMETER>()
.HasMany(e => e.REPORTS)
.WithMany(e => e.PARAMETERS)
.Map(m => m.ToTable("LNK_REPORT_PARAMETER").MapLeftKey("Parameter_Id").MapRightKey("Report_Id"));
}
}
[Table("REPORTS")]
public partial class REPORT
{
public REPORT()
{
PARAMETERS = new HashSet<PARAMETER>();
ROLES = new HashSet<ROLE>();
}
[Key]
public int Report_Id { get; set; }
public DateTime LastModified { get; set; }
public bool Inactive { get; set; }
public string ReportName { get; set; }
public string ReportDescription { get; set; }
public string Template { get; set; }
public string StoredProcedureName { get; set; }
public virtual ICollection<ROLE> ROLES { get; set; }
public virtual ICollection<PARAMETER> PARAMETERS { get; set; }
}
[Table("PARAMETERS")]
public partial class PARAMETER
{
public PARAMETER()
{
REPORTS = new HashSet<REPORT>();
}
[Key]
public int Parameter_Id { get; set; }
public bool Required { get; set; }
public string ParameterName { get; set; }
public string ParameterType { get; set; }
public string DisplayLabel { get; set; }
public virtual ICollection<REPORT> REPORTS { get; set; }
}
[Table("ROLES")]
public partial class ROLE
{
public ROLE()
{
PERMISSIONS = new HashSet<PERMISSION>();
USERS = new HashSet<USER>();
REPORTS = new HashSet<REPORT>();
}
[Key]
public int Role_Id { get; set; }
[Required]
public string RoleName { get; set; }
public string RoleDescription { get; set; }
public bool IsSysAdmin { get; set; }
public DateTime? LastModified { get; set; }
public virtual ICollection<PERMISSION> PERMISSIONS { get; set; }
public virtual ICollection<USER> USERS { get; set; }
public virtual ICollection<REPORT> REPORTS { get; set; }
}
我们在 RBAC 模型中引入了两个新类,名为 'REPORT
' 和 'PARAMETER
';'ROLE
' 类已被重构,包含一个类型为 ICollection<REPORT>
的 REPORTS
属性。此属性将包含与用户角色关联的报告。
扩展我们的 RBACUser 类
现在我们已经更新了数据库上下文模型以包含新的报告表,我们需要修改我们的 RBACUser
类以公开 Reports 属性和提供用户报告权限的功能。
注意:本文档中提供的代码片段都是最小化的,旨在说明附加代码并避免重复第一部分中定义的代码。代码中用灰色高亮显示的节(参见本文档的 pdf 版本)说明了为 RBAC 报告添加的附加代码。请参考可下载的示例项目以查看完整的代码列表及其在项目中的上下文。
public class RBACUser
{
public int User_Id { get; set; }
public bool IsSysAdmin { get; set; }
public string Username { get; set; }
private List<UserRole> Roles = new List<UserRole>();
public RBACUser(string _username)
{
this.Username = _username;
this.IsSysAdmin = false;
GetDatabaseUserRolesPermissions();
}
private void GetDatabaseUserRolesPermissions()
{
using (RBAC_Model _data = new RBAC_Model())
{
USER _user = _data.USERS.Where(u => u.Username == this.Username).FirstOrDefault();
if (_user != null)
{
this.User_Id = _user.User_Id;
foreach (ROLE _role in _user.ROLES)
{
UserRole _userRole = new UserRole { Role_Id = _role.Role_Id, ... };
foreach (PERMISSION _permission in _role.PERMISSIONS)
{
_userRole.Permissions.Add(
new PERMISSION { Permission_Id = _permission.Permission_Id, ... });
}
foreach (REPORT _report in _role.REPORTS)
{
_userRole.Reports.Add(
new REPORT { Report_Id = _report.Report_Id, ... });
}
this.Roles.Add(_userRole);
if (!this.IsSysAdmin)
this.IsSysAdmin = _role.IsSysAdmin;
}
}
}
}
public List<REPORT> GetReports()
{
List<REPORT> _retVal = new List<REPORT>();
foreach (UserRole _role in this.Roles)
{
foreach (REPORT _report in _role.Reports)
{
if (_report.Inactive == false)
{
if (!_retVal.Contains(_report))
{
_retVal.Add(_report);
}
}
}
}
return _retVal;
}
}
public class UserRole
{
public int Role_Id { get; set; }
public string RoleDescripton { get; set; }
public List<PERMISSION> Permissions = new List<PERMISSION>();
public List<REPORT> Reports = new List<REPORT>();
}
RBACUser
类已被扩展,包含一个名为 GetReports() 的新函数,该函数返回基于用户角色的可用报告。
RBAC 报告解决方案
我们的报告解决方案包括四个主要部分,如下面的图所示。RBACUser
类现已扩展,以公开名为 GetReports 的类方法,该方法返回与用户角色相关联的 REPORT
对象集合,从而实现基于角色的报告。
|
可下载的示例项目从主报告菜单驱动所示的流程图。点击报告菜单(如果允许)会调用 在可用的用户报告中,用户可以选择一份报告。将调用 点击“报告筛选器预览”页面上所选报告的“生成报告”按钮,会将每个筛选器参数的名称-值对合并为一个字符串值,然后调用 |
生成的报告数据将与控制器视图“DefaultResultsTemplate.cshtml”合并,以生成报告结果页面返回给用户(标识为过程 4)。报告数据是通过调用报告管理过程中分配给该报告的数据库存储过程生成的。
将筛选参数与报告关联是可选的,报告可以与没有输入参数的数据库存储过程关联,因此不需要筛选参数。此外,可以为每个报告使用自定义结果模板文件 (.cshtml) 来代替默认结果模板“DefaultResultsTemplate.cshtml”。但是,任何自定义结果模板文件都必须在部署前编译到应用程序中,否则当控制器操作尝试将报告数据与不存在的指定视图模板合并时,将生成运行时错误。
我们需要将每个筛选器参数的名称-值对合并为一个字符串,然后将此值作为单个字符串传递,以便只使用一个函数;即使使用客户端滑块函数参数映射到调用服务器端逻辑的 JavaScript 函数(标识为过程 3),函数重载也无法可靠工作。
最后,我们能够将显示的报告结果导出为 CSV 文件(标识为过程 5)。
ReportsController.cs 代码列表
我们的 RBAC 报告解决方案实现为一个名为 Reports 的新控制器,以便将报告功能与 Admin 控制器中的管理功能分离。然而,报告管理是管理控制器的一部分。
以下列表详细介绍了我们报告解决方案的主要控制器操作方法,如上一个流程图所示。我们稍后将仔细研究 ExecuteReportviaSP
方法。
ReportsController.cs
[RBAC]
public class ReportsController : CommonControllerBase
{
private RBAC_Model Database = new RBAC_Model();
public ActionResult Index()
{
//Called from flow diagram process 1
return View(this.GetReports());
}
public ActionResult Preview(int id)
{
//Called from flow diagram process 2
return View(this.GetReports().Where(p => p.Report_Id == id).FirstOrDefault());
}
[HttpGet]
public ActionResult Execute(int id, string rawParams)
{
//Called from flow diagram process 3
//Generates flow diagram process 4
return ExecuteReportviaSP(this.GetReports().Where(p => p.Report_Id == id).FirstOrDefault(), rawParams);
}
[HttpPost]
public ActionResult ExportData(FormCollection form)
{
//Called from flow diagram process 5
List<dynamic> _list = TempData["ModelData"] as List<dynamic>;
try
{
int _recordsExported = DynamicDataExport2CSV.Export(_list);
return RedirectToAction("Error", "Unauthorised", new RouteValueDictionary(
new { _errorMsg = string.Format("Records Exported: {0}", _recordsExported) }));
}
catch (Exception ex)
{
return RedirectToAction("Error", "Unauthorised", new RouteValueDictionary(
new { _errorMsg = ex.Message }));
}
}
}
Index.cshtml 视图代码列表,对应 ReportsController.Index() 操作方法
以下列表详细介绍了我们的主要控制器操作方法视图;控制器操作方法会将 REPORT
对象集合传递给视图 Index.cshtml。回想一下第一部分,每个控制器操作都必须返回一个操作结果来响应浏览器请求。控制器将搜索与操作方法同名的视图文件名,除非我们显式指定一个要渲染的替代视图。
|
点击报告菜单选项(如果允许)将显示“用户报告摘要”页面,其中详细列出了用户角色可用的报告。此时将调用 |
ReportsController.cs
public ActionResult Index() { //Called from flow diagram process 1 return View(this.GetReports()); }
|
由于我们没有指定要渲染的视图,因此将使用调用的控制器操作方法名作为视图名,即使用视图 Index.cshtml。 |
Index.cshtml
@model IEnumerable<REPORT> @{ ViewBag.Title = "Reports"; } <script type="text/javascript"> $(document).ready(function () { $(":input[type='button']").button(); $("#expanderHead").click(function () { $("#expanderContent").slideToggle(); if ($("#expanderSign").text() == "+") { $("#expanderSign").html("−") } else { $("#expanderSign").text("+") } }); }); function GetReportPreview(Report_Id) { $("#expanderHead").click(); $('#preview').html('<h2>Please wait, loading Report...</h2>'); $.get('/Reports/Preview?report_id=' + Report_Id, function (data) { $('#preview').html(data); }); }; </script> <div id="expanderHead">Report List <span id="expanderSign" style="color: Black; font-size: larger">-</span></div> <div id="expanderContent" style="padding-left: 30px"> <fieldset style="padding: 1em; font: 100%; color: Black; border-color: Black; border-style: solid"> <legend><strong>User Reports Summary (Role Based)</strong></legend> Select a report from the list above <table id="ReportTable"> <tbody> <ul> @foreach (var item in Model) { <tr> <td width="10px"></td> <td> <li> <a onclick="GetReportPreview(@item.Report_Id);">@item.ReportName</a> <br />@Html.DisplayFor(modelItem => item.ReportDescription) </li> </td> </tr> } </ul> </tbody> </table> </fieldset> </div> <br /> <div id="preview" style="text-align: center;" />
用户可以从列表中选择任何报告。如果报告定义了筛选参数,用户将看到“报告筛选器预览”页面,其中显示了每个定义的筛选参数。对于定义为“必需”的筛选参数,用户必须输入所有“必需”字段才能继续到报告结果页面。对于定义为“非必需”的筛选参数,用户可以选择留空,该值将不会传递到存储过程。
Preview.cshtml 视图代码列表,对应 ReportsController.Preview() 操作方法
以下列表详细介绍了我们“报告筛选器预览”页面的主要控制器操作方法视图;将调用 ReportsController
类中的 Preview
控制器操作,并将所选报告的 Report_Id
传递。Preview
控制器操作将 REPORT
对象作为视图的模型传递;REPORT
对象包含一个 PARAMETERS
属性,其中列出了关联的报告筛选器参数(通过报告管理定义)。
ReportsController.cs
public ActionResult Preview(int report_id) { //Called from flow diagram process 2 return View(this.GetReports().Where(p => p.Report_Id == report_id).FirstOrDefault()); }
Preview.cshtml
@model REPORT <script type="text/javascript"> $(document).ready(function () { jQuery.ajaxSettings.traditional = true; $(":input[type='button']").button(); $("#ReportPreview").click(function () { GetReportParameters2Execute(); }); }); // Function to retrieve all filter parameters from the page to pass to controller method function GetReportParameters2Execute() { var ReportURL = '/Execute?report_id=' + $("#Report_Id").val(); var FilterParameters = ""; var bContinue = true; $('.filter-input').each(function (i, obj) { if ((this == null || $(this).val() == "") & $(this).attr('required') == 'required') { alert($(this).attr('displaylabel') + ' must be entered'); bContinue = false; return false; } else { FilterParameters = FilterParameters + "\\" + $(this).attr('id') + "=" + $(this).val(); } }); //Don't continue if mandatory fields are not entered... if (bContinue == false) return false; FilterParameters = FilterParameters.substring(1); $('#preview').html('<h2>Please wait, generating report...</h2>'); ReportURL = ReportURL + '&rawParams=' + FilterParameters; $.get('/Reports' + ReportURL, function (data) { $('#preview').html(data); }); }; </script> <input type="hidden" name="ReportId" id="ReportId" value="@Model.Report_Id" /> <div style="text-align: left; background-color: Black; font-size: 1.2em; color: #fff;"> <strong>Report Title:</strong> @Html.DisplayFor(model => model.ReportName)<br /> <br /> @{if (Model.ReportDescription != null) { <strong>Description:</strong> @Html.DisplayFor(model => model.ReportDescription) } } </div> <br /> @{if (Model.PARAMETERS.Count > 0) { <div class="row" style="text-align: left;"> <strong> Please enter optional report parameters below and then click 'Generate Report' to display your report results...<br /> * - Indicates a required field </strong> </div>} } <div class="panel" style="text-align: left;"> <br /> @Html.HiddenFor(model => model.Report_Id) @Html.Partial("_filterControls", Model) <br /> <input type="button" value="Generate Report" name="ReportPreview" id="ReportPreview" /> </div> <div class="row"> <div id="reportError"> </div> </div>
|
Preview.cshtml 视图通过 部分视图使用作为模型传递的 |
_filterControls.cshtml
@model REPORT <script> $(function () { $('[datepicker]').datepicker(); }); </script> <table> @foreach (var _parameter in Model.PARAMETERS) { <tr> <td> @{ string controlLabel = _parameter.DisplayLabel; if (_parameter.Required) { controlLabel = controlLabel + "*"; } } @Html.Raw(controlLabel) </td> <td> @{ string control = string.Format("<input type='{0}' id='{1}' name='{1}' displaylabel='{2}' class='filter-input'", "text", _parameter.ParameterName, _parameter.DisplayLabel); if (_parameter.Required) { control += " required='required'"; } if (_parameter.ParameterType.ToLower() == "date" || _parameter.ParameterType.ToLower() == "datetime") { control += "datepicker"; } control += control + " />"; @Html.Raw(control) } </td> </tr> } </table>
生成的“报告筛选器预览”页面显示报告的关联筛选参数。筛选参数通过“示例项目”部分演示的报告管理来定义。定义为必需的筛选参数必须输入,否则用户将看到验证错误消息。
通过“报告管理”选项定义筛选参数时,必须使用与数据库存储过程相同的参数名称,否则应用程序尝试执行存储过程时将生成错误。总的来说,数据库管理员将指定存储过程的参数名称;我们只需要确保为报告的筛选参数名称使用相同的名称。考虑以下数据库存储过程定义。
CREATE PROCEDURE [dbo].[spTest]
@Manufacturer VARCHAR(30) = null,
@Model VARCHAR(30) = null,
@fromSaleDate [VARCHAR] (19) = null,
@toSaleDate [VARCHAR] (19) = null
AS
BEGIN
DECLARE @SQL varchar(1000)
SET @SQL = 'SELECT ManufacturerName,
Model,
CAST(EngineSize AS decimal(10,1)) AS [EngineSize],
EnginePower,
FuelType,
DATENAME(Month, DATEADD(Month, DATEPART(Month, Date), 0) - 1) AS [Month],
SUM(Price) AS [Total Sales],
COUNT(*) AS [Total Orders]
FROM [SALES]'
...
...
SET @SQL = @SQL + 'GROUP BY DATEPART(m, Date), ManufacturerName, Model, EngineSize, ... '
SET @SQL = @SQL + 'ORDER BY ManufacturerName, Model, DATEPART(m, Date), EngineSize, ...'
EXEC(@SQL)
END
存储过程‘spTest’定义了四个参数,我们必须通过“报告筛选器预览”页面将它们暴露给用户。
注意:上面的示例数据库存储过程查询演示的‘SALES’数据库表,该表与 RBAC 数据模型没有关联。根本上,示例存储过程表示数据库中定义的存储过程,该过程查询您的数据库表,可以通过我们的 RBAC 框架作为应用程序报告公开,而无需重新编译应用程序。
现在我们来看一下下面示例的报告筛选器参数“车辆制造商”;报告筛选器参数存储在 PARAMETERS 表中,每个参数通过 LNK_REPORT_PARAMETER 链接表与 REPORTS 表相关联。‘Vehicle Manufacturer’筛选器参数与Sales Summary报告相关,该报告将报告的数据库存储过程名称指定为‘spTest’。
SELECT p.* FROM PARAMETERS p
JOIN LNK_REPORT_PARAMETER lnk ON lnk.Parameter_Id=p.Parameter_Id
JOIN REPORTS r ON r.Report_Id=lnk.Report_Id
WHERE r.ReportName='Sales Summary'
如果我们输入的‘Vehicle Manufacturer’ParameterName 是‘ManufacturerName’而不是‘Manufacturer’,我们在尝试生成报告时会收到以下错误。这是因为存储过程无法将传递的输入参数‘ManufacturerName’映射到任何已定义的存储过程参数。
在这种情况下,‘Vehicle Manufacturer’ParameterName 需要定义为‘Manufacturer’,从而匹配存储过程的参数名称。
DefaultResultsTemplate.cshtml 视图代码列表,对应 ReportsController.Execute() 操作方法
以下列表详细介绍了控制器操作方法的视图;将调用 ReportsController
类中的 Execute
控制器操作,并将所选报告的 Report_Id
和用户输入的筛选参数(如果适用)传递。控制器操作方法会解析 rawParams
参数中的每个名称-值对,以用作筛选器参数。使用用户输入的筛选值执行存储过程。如果报告没有定义筛选参数,则 rawParams
参数将传递为空字符串值,因此不会将参数传递给存储过程。
ReportsController.cs
[HttpGet]
public ActionResult Execute(int report_id, string rawParams)
{
//Called from flow diagram process 3
//Generates flow diagram process 4
return ExecuteReportviaSP(this.GetReports().Where(p => p.Report_Id == report_id).FirstOrDefault(), rawParams);
}
private ActionResult ExecuteReportviaSP(REPORT _report, string rawParams, string _defaultReportTemplate = "DefaultResultsTemplate")
{
List<dynamic> _list = new List<dynamic>();
string _reportName = _report.Template;
try
{
_list = CommonSql.ExecuteStoredProcedure(_report, rawParams, this);
}
catch (Exception ex)
{
return RedirectToAction("Error", "Unauthorised", new RouteValueDictionary(new { _errorMsg = ex.Message }));
}
string _targetFile = string.Format("{0}/{1}.cshtml", Server.MapPath("~/Views/Reports"), _reportName);
if (!System.IO.File.Exists(_targetFile))
{
_reportName = _defaultReportTemplate;
}
return View(_reportName, _list);
}
从控制器操作 Execute
中调用的 ExecuteReportviaSP
函数运行静态函数 CommonSql.ExecuteStoredProcedure
,该函数负责解析 rawParams
参数中的每个名称-值对并执行为报告定义的数据库存储过程。数据库存储过程的结果作为动态对象集合返回;集合传递给视图以在屏幕上呈现结果。
ExecuteStoredProcedure
函数定义为静态类 CommonSql
的一部分,该类与应用程序模型解耦,以便返回动态数据并使该类可重用。该函数相对简单,利用 SqlCommand
.NET 框架类来执行指定的存储过程和关联的参数。
DefaultResultsTemplate.cshtml
@model List<dynamic> <div id="printableArea" style="text-align: left;"> <h2> @ViewBag.ReportName </h2> </h3>(@Model.Count() matching records) <script type="text/javascript"> $(document).ready(function () { ... $('#msgContainer').hide(); }); </script> @using (Html.BeginForm("ExportData", "Reports", FormMethod.Post, new { enctype = "multipart/form-data" })) { TempData["ModelData"] = Model; <div> <table id="ResultsTable" class="audittable" style="width: 100%"> <thead> @foreach (DynamicDataRow item in Model) { foreach (DynamicDataObject col in item.Columns) { <th> @col.Name </th> } break; } </thead> @foreach (DynamicDataRow item in Model) { <tr> @foreach (DynamicDataObject col in item.Columns) { <td> @col.Value </td> } </tr> } </table> </div> <br /> <input type="submit" id="exportdata" value="Export to CSV" /> } <br /> </div>
|
|
自定义报告结果模板
如果您需要自定义报告,可以“替换”标准结果模板为自己的自定义版本。但是,您需要在设计时添加一个新的视图,进行自定义,重新编译,然后重新部署应用程序。然后,您可以为您的报告应用新模板。
让我们以我们的‘Sales Summary’报告为例,该报告默认使用‘DefaultResultsTemplate’视图。我们将在 Reports 文件夹中引入一个名为‘customSalesSummary’的新视图,并在其中自定义视图模板。
在 Visual Studio 中使用解决方案资源管理器,点击应用程序“Views”文件夹中的“Reports”文件夹。右键单击并选择“添加 >> 视图…”。将显示以下对话框。将视图名称输入为‘customSalesSummary’,然后点击‘添加’。
将在 Reports 文件夹中添加一个新的视图。自定义您的视图并保存更改。如果您自定义视图以使用图像,请确保将图像包含在项目中,否则部署应用程序时图像将不会被部署。在 Visual Studio 中使用解决方案资源管理器,在项目中找到您的图像文件夹(通常在‘Content’文件夹内)并添加您的图像。重新编译应用程序并重新部署。
注意:在运行我们的报告之前,我们需要更新‘Sales Summary’定义,通过位于系统管理菜单下的报告菜单指定我们的新模板。
customSalesSummary.cshtml
@model List<dynamic> <img src="@Url.Content("~/Content/Images/customizedlogo.png")" align="left" ... /> <div id="printableArea" style="text-align: left;"> <h1> @ViewBag.ReportName </h1> (@Model.Count() matching records) <script type="text/javascript"> $(document).ready(function () { $("#ResultsTable tr:even").css("background-color", "#EBF0FF"); $("#ResultsTable tr:odd").css("background-color", "#ffffff"); }); </script> <div> <table id="ResultsTable" class="audittable" style="width: 100%"> <thead> @foreach (DynamicDataRow item in Model) { foreach (DynamicDataObject col in item.Columns) { <th> @col.Name </th> } break; } </thead> @foreach (DynamicDataRow item in Model) { <tr> @foreach (DynamicDataObject col in item.Columns) { <td> @if (col.DataType == "money") { @Convert.ToDecimal(col.Value).ToString("#,##0.00"); } else { @col.Value } </td> } </tr> } </table> </div> <h2>Total Sales £ @GetSalesTotal(Model).ToString("#,##0.00")</h2> <br /> </div> @functions { public decimal GetSalesTotal(List<dynamic> _obj) { decimal _retVal = 0; try { foreach (DynamicDataRow item in _obj) { _retVal += Convert.ToDecimal(item.GetColumnValue(6)); } } catch (Exception) { throw; } return _retVal; } }
我们现在已经使用自定义报告结果模板自定义了 Sales Summary 报告。我们可以随时切换回默认报告结果模板,反之亦然,只需更改报告定义即可。由于视图已存在于应用程序二进制文件中,因此无需重新编译或部署应用程序。
DynamicDataRow 类
DynamicDataRow
类维护一个 DynamicDataObject
对象列表;DynamicDataObject
对象指定属性以表示列的名称、值和数据类型。列名属性用作报告列标题,值属性用于报告列值,数据类型属性用于格式化。所示的‘Sales Summary’报告使用逗号分隔符格式化‘Total Sales’列;我们可以通过数据类型属性确定该列是否代表‘money’值,并相应地进行格式化。
DynamicDataExport.cs
public class DynamicDataObject
{
public readonly string Name;
public readonly string Value;
public readonly string DataType;
public DynamicDataObject(string _colName, string _colValue, string _colType = "string")
{
Name = _colName;
Value = _colValue;
DataType = _colType;
}
}
public class DynamicDataRow
{
public List<DynamicDataObject> Columns = new List<DynamicDataObject>();
public int ColumnCount
...
public void AddColumn(string _columnName, dynamic _columnValue, string _columnDataType)
...
public string GetColumnValue(string _columnName, string _defaultValue = "")
...
public string GetColumnValue(int _idx)
...
public int GetColumnValueAsInt(string _columnName, int _defaultValue = 0)
...
public string GetColumnName(int _idx)
...
}
这两个类都可以通过添加自定义属性和函数来扩展。CommonSql.ExecuteStoredProcedure
函数填充 DynamicDataRow
对象,该对象代表返回的数据库结果集中的单行。该函数返回一个 DynamicDataRow
对象集合,作为 List<dynamic>
传递给底层视图作为视图模型,如下面的代码片段所示。
CommonSql.cs
public static List<dynamic> ExecuteStoredProcedure(REPORT _report, string _rawParams, Controller _controller) { List<dynamic> _dataRows = new List<dynamic>(); //Read parameters... string[] _parameters = _rawParams.Split('\\'); foreach (string _param in _parameters) ... sqlConn.Open(); SqlDataReader dbReader = command.ExecuteReader(); while (dbReader.Read()) { DynamicDataRow _row = new DynamicDataRow(); for (int i = 0; i < dbReader.FieldCount; i++) { _row.AddColumn(dbReader.GetName(i), DbUtil.GetValue(dbReader, i), dbReader.GetDataTypeName(i).ToString()); } _dataRows.Add(_row); } sqlConn.Close(); }
DynamicDataExport2CSV 类
DynamicDataExport2CSV
类将 DynamicDataRow
对象集合导出为纯文本文件,使用逗号分隔值 (CSV),其中每个记录由一行或多行字段组成,字段之间用逗号分隔。文本文件中的每一行代表一个记录。
DefaultResultsTemplate.cshtml
@using (Html.BeginForm("ExportData", "Reports", FormMethod.Post, new { enctype = "multipart/form-data" })) { TempData["ModelData"] = Model; ... <input type="submit" id="exportdata" value="Export Results to CSV" /> }
ReportsController.cs
[HttpPost] public ActionResult ExportData(FormCollection form) { List<dynamic> _data = TempData["ModelData"] as List<dynamic>; try { int _recordsExported = DynamicDataExport2CSV.Export(_data); return RedirectToAction("Error", "Unauthorised", new RouteValueDictionary(new { _errorMsg = string.Format("Records Exported: {0}", _recordsExported) })); } catch (Exception ex) { return RedirectToAction("Error", "Unauthorised", new RouteValueDictionary(new { _errorMsg = ex.Message })); } }
DynamicDataExport.cs
public class DynamicDataExport2CSV : DynamicDataExportBase { public static int Export(List<dynamic> objList, string _filename = "DataExport") { int _retVal = 0; try { if (objList != null && objList.Count > 0) { SendHttpContextHeaderInfo(_filename); WriteRowData(objList[0], true); foreach (DynamicDataRow obj in objList) { WriteRowData(obj); } HttpContext.Current.Response.End(); _retVal = objList.Count; } } catch (Exception ex) { throw ex; } return _retVal; } private static void WriteRowData(dynamic obj, bool _columnNames = false) { StringBuilder _data = new StringBuilder(); foreach (DynamicDataObject _column in obj.Columns) { if (_columnNames) AddComma(_column.Name, _data); else AddComma(_column.Value, _data); } _data = RemoveLastComma(_data); HttpContext.Current.Response.Write(_data.ToString()); HttpContext.Current.Response.Write(Environment.NewLine); } }
点击‘Export Results to CSV’按钮将把代表报告结果的 DynamicDataRow
对象集合导出为纯文本文件。DynamicDataExport2CSV
类和相关的 DynamicData
对象是简单的实现,可扩展性强。在可能的情况下,DynamicDataRow
对象集合被传递为 List<dynamic>
,以避免强制执行“类型安全”的参数传递,从而提供以最少的工作量实现更改来扩展系统的能力。
注意:通过不强制执行“类型安全”的参数传递,您可以使用 DynamicDataExport2CSV
类传递自定义对象集合,而无需更改函数的类型签名;需要在函数体内进行修改,以引用自定义对象的属性和/或函数。将参数作为动态传递的缺点是,编译器在编译期间不会“检测”不存在的引用对象属性,而当执行到该行代码时会抛出异常。
DECLARE @SQL varchar(1000)
SET @SQL = 'SELECT ManufacturerName,
Model,
CAST(EngineSize AS decimal(10,1)) AS [EngineSize],
EnginePower,
FuelType,
DATENAME(Month, DATEADD(Month, DATEPART(Month, Date), 0) - 1) AS [Month],
SUM(Price) AS [Total Sales],
COUNT(*) AS [Total Orders]
FROM [SALES]'
...
...
SET @SQL = @SQL + 'GROUP BY DATEPART(m, Date), ManufacturerName, Model, EngineSize, EnginePower, FuelType '
SET @SQL = @SQL + 'ORDER BY ManufacturerName, Model, DATEPART(m, Date), EngineSize, EnginePower, FuelType'
EXEC(@SQL)
List<dynamic> _list = CommonSql.ExecuteStoredProcedure(_report, rawParams, this);
DynamicDataExport2CSV.Export(_list);
导出的 CSV 文件将包含来自数据库存储过程的数据,包括列标题。导出的 CSV 文件可以被大多数电子表格程序导入,如下图所示。
我们的报告解决方案的关键优势在于,“默认”报告模板不与任何特定的数据模型绑定,从而提供了更高的代码可重用性。‘默认’报告模板将显示任何存储过程的输出,而无需对报告模板进行任何更改,即使存储过程后来更改为返回其他列。在绝大多数情况下,应用程序生成的报告通常需要显示反映数据库中数据的基于列的表。基于列的表是最常见的报告输出形式。基于列的表是顺序的、二维的列表。这并不意味着表中的数据是简单或不复杂的;您可以在表中呈现大量数据。数据库存储过程通常驱动报告逻辑,其中连接和/或交叉引用了多个表,以返回所需数据。然而,我们确实能够自定义报告,可以在我们的“表示”报告模板层中编写额外的逻辑,或者根据列数据类型进行特定格式化。
特别是,我们的 RBAC 报告框架提供了一个满足我们大部分报告需求的报告解决方案。我们的解决方案是独立于供应商的,不需要“额外”的许可。它可以与几乎任何数据源(SQL Server、MySql 等)配合使用,并且可以扩展以满足特定项目需求。此外,我们还可以导出报告数据,其中导出功能可以授予给特定的角色和/或权限,使用我们在 RBACUser 类中公开的自定义‘HasRole’和‘HasPermission’方法。
替代供应商报告解决方案
我们现在将修改我们的报告解决方案以集成 Microsoft 的 SQL Server Reporting Services (SSRS) 解决方案。SSRS 是 Microsoft 的一个基于服务器的报告平台,允许我们创建和管理各种类型的报告,并以多种格式交付它们。SSRS 使用户能够快速轻松地生成包含表格和图表的报告,或更复杂的数据可视化,使用图表、地图和迷你图。SSRS 提供了一个完整的环境来创建和发布用于查看的复杂报告。
我们可以直接从报告服务网站(称为 Report Manager)呈现完成的报告,或者我们可以使用 ASP.NET ReportViewer Web 控件在我们的 Web 应用程序中直接查看它们。
SQL Server 报告服务 (SSRS) 概述
SSRS 部署必须与 SQL Server 实例相关联,因为报告数据将从此源获取。我们还需要一个 Report Server Web Service 的位置,它可以与数据库在同一服务器上,也可以在不同的服务器上。无论我们选择哪个服务器,我们都可以访问 Report Manager 网站,该网站允许我们部署和管理报告。报告发布后,最终用户会发送一个 HTTP 请求来获取报告,包括任何必需的参数。SSRS 服务器找到报告的元数据,并向数据源发送数据请求。数据源返回的数据与报告定义合并成一份报告。当报告生成时,它会被返回给客户端。
RBAC 与 SSRS 集成
本节不详细介绍 SSRS 的安装或配置,而是假设您已正确配置并运行 SSRS。本节将解释如何调整 RBAC 报告解决方案以集成您的 SSRS 解决方案,以便在功能包含 RBAC 框架的 Web 应用程序中直接查看报告。
用户从可用报告列表中选择一份报告。如果报告定义了筛选参数,用户将看到“报告筛选器预览”页面,显示每个定义的筛选参数,与之前相同。点击“报告筛选器预览”页面上的“生成报告”按钮会将每个筛选器参数的名称-值对合并到报告 URL 字符串中。URL 字符串包含 Report Server Web Service 的位置和报告的名称。最终的 URL 字符串作为 HTTP 请求发送到 SSRS,然后 SSRS 返回结果并显示在单独的浏览器窗口中。这次没有控制器操作方法被执行,而是通过客户端浏览器中的 JavaScript 函数‘GetSSRSReportParameters2Execute()
’将筛选参数附加到 URL 并请求。‘最终’URL 的格式为:
"http://.../ReportServer/Pages/ReportViewer.aspx?salessummary¶m1=abc¶m2=123"
'http://…/ReportServer/Pages/ReportViewer.aspx' 代表 Report Server Web Service 的位置,并存储在配置文件的设置中,'salessummary' 代表报告名称,存储在给定报告的 REPORTS 表的“Template”字段中,每个筛选参数都像以前一样关联。
GetSSRSReportParameters2Execute() JavaScript 函数
以下列表详细介绍了 Preview.cshtml 中位于 GetSSRSReportParameters2Execute()
的 JavaScript 函数。‘报告筛选器预览’页面由 ReportsController
中的 Preview
控制器操作渲染,并将 REPORT
对象作为视图的模型传递。
Web.config
<?xml version="1.0"?> <configuration> <appSettings> <add key="UnobtrusiveJavaScriptEnabled" value="true"/> <add key="ReportViewerUrl" value="http://yourreportserver/ReportServer/Pages/ReportViewer.aspx"/> ... </appSettings> ...
ReportsController.cs
public ActionResult Preview(int report_id) { ViewBag.ReportUrl = ConfigurationManager.AppSettings.Get("ReportViewerUrl"); //Called from flow diagram process 2 return View(this.GetReports().Where(p => p.Report_Id == report_id).FirstOrDefault()); }
Preview.cshtml
@model REPORT <input type="hidden" name="SSRSUrl" id="SSRSUrl" value="@ViewBag.ReportUrl" /> <input type="hidden" name="ReportId" id="ReportId" value="@Model.Report_Id" /> <input type="hidden" name="SSRSReportName" id="SSRSReportName" value="@Model.Template" /> <script type="text/javascript"> $(document).ready(function () { jQuery.ajaxSettings.traditional = true; $(":input[type='button']").button(); $("#ReportPreview").click(function () { GetSSRSReportParameters2Execute(); }); }); // Function to retrieve all filter parameters from the page to pass to Reporting Services function GetSSRSReportParameters2Execute() { var SSRSReportURL = $("#SSRSUrl").val(); var FilterParameters = ""; var bContinue = true; $('.filter-input').each(function (i, obj) { if ((this == null || $(this).val() == "") & $(this).attr('required') == 'required') { alert($(this).attr('displaylabel') + ' must be entered'); bContinue = false; return false; } else { if ($(this).val() != "") FilterParameters = FilterParameters + "&" + $(this).attr('id') + "=" + $(this).val(); } }); //Don't continue if mandatory fields are not entered... if (bContinue == false) return false; SSRSReportURL = SSRSReportURL + "?" + $("#SSRSReportName").val() + FilterParameters; window.open(SSRSReportURL, '', 'scrollbars=no,menubar=no,resizable=yes,toolbar=no, location=no,status=no'); }; </script> ...
代码中用灰色高亮显示的节(参见本文档的 pdf 版本)说明了“添加”到与 SSRS 集成的代码。Preview
控制器操作已被修改,通过 ViewBag 参数 ReportUrl
传递 ReportViewerUrl
配置设置。‘Preview.cshtml’模板已被修改,以包含新的 JavaScript 函数 GetSSRSReportParameters2Execute()
。‘Generate Report’按钮已将其‘onclick()’事件分配给执行新函数。
当按下‘Generate Report’按钮时,将执行 GetSSRSReportParameters2Execute()
函数。该函数通过附加传递的 ReportViewerUrl
配置设置、所选报告的 SSRS 报告名称以及显示在‘Report Filter Preview’页面上的输入筛选参数来构建 URL。生成的 URL 作为 HTTP 请求发送到 SSRS,SSRS 返回结果并显示在单独的浏览器窗口中。
注意:存储应用程序设置在应用程序配置文件(Web.config)中的另一种方法是将设置存储在数据库表中。无论哪种情况,我们都可以通过我们的应用程序将这些设置暴露出来,从而允许将修改保存回数据库表或配置文件。
通过 SSRS 生成报告
进行了上述修改后,我们现在可以集成并通过 SSRS 生成报告,如下图所示。前面各节中演示的‘Sales Summary’报告已在 SSRS 中使用 SQL Server Report Builder 重构。
|
如果我们现在点击‘Generate Report’按钮,我们的报告将通过 SSRS 生成并在新浏览器窗口中显示。 |
示例项目
可下载的示例项目基于原始示例项目(在第一部分中可用),以包含动态的基于角色的报告以及后续的角色/报告维护。示例项目包含一个位于系统管理菜单下的报告菜单。点击报告菜单将加载“报告摘要”页面(如下图所示),该页面允许进行应用程序报告的 CRUD 维护以及角色关联。
应用程序报告
要定义新的应用程序报告,请点击位于系统管理菜单下的报告菜单。将显示报告表,其中显示已定义的每个应用程序报告。从此屏幕,您将能够定义新报告、编辑或删除现有报告。
要定义新的应用程序报告,只需点击“创建报告”按钮即可加载以下屏幕。
根据需要填充每个字段,并确保输入到 StoredProcedureName 字段的数据库存储过程名称在应用程序部署时存在或将存在于数据库中。
报告角色分配
定义应用程序报告后,可以使用与报告关联的编辑图标将用户角色和筛选参数分配给报告。
可以随时将角色与报告关联或取消关联。通过从下拉列表中选择角色并按“添加角色”按钮来分配单个角色;可以使用垃圾桶图标取消分配不需要的角色。同样,可以通过“添加参数”按钮将报告筛选参数与报告关联。
报告筛选器参数分配
可以随时为报告创建和删除筛选参数。只需点击“添加参数”按钮即可扩展“报告筛选器参数”面板,如下图所示。
根据需要填充每个字段,并确保“参数名称”与我们将要从中传递值的存储过程的参数名称匹配。要取消当前筛选参数的添加,请点击取消图标。第二次点击“添加参数”按钮可将值存储在数据库中并与之关联。可以使用垃圾桶图标删除不需要的参数。
演示销售表
可下载的示例项目包含一个名为‘RBAC_CarSalesDemoSchema.sql’的 SQL 脚本文件。该脚本创建一个名为‘SALES’的表,用示例数据填充该表,并创建一个名为‘spTest’的数据库存储过程。‘SALES’表和附带的存储过程代表您数据库中的表/存储过程,并且与 RBAC 模型无关。因此,在将 RBAC 集成到您的项目中时,您不需要运行此 SQL 脚本,因为您将要报告的表已经存在于您的数据库中。
--Create demo SALES table... CREATE TABLE [dbo].[SALES]( [Sale_Id] [int] IDENTITY(1,1) NOT NULL, [Date] [date] NOT NULL DEFAULT GETDATE(), [Price] [money] NOT NULL DEFAULT 0, [ManufacturerName] [nvarchar](30) NOT NULL, [Model] [nvarchar](30) NOT NULL, [Colour] [nvarchar](30) NOT NULL, [EngineSize] decimal(3,1) NOT NULL, [EnginePower] [int] NOT NULL, FuelType] [nvarchar](10) NOT NULL, [LHD] [bit] NOT NULL default 0, CONSTRAINT [PK_SALES] PRIMARY KEY CLUSTERED ( [Sale_Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO --Create Stored Procedure 'spTest' to query the SALES table... --spTest will be used by our 'Sales Summary' report... CREATE PROCEDURE [dbo].[spTest] @Manufacturer VARCHAR(30) = null, @Model VARCHAR(30) = null, @fromSaleDate VARCHAR (19) = null, @toSaleDate VARCHAR (19) = null AS BEGIN DECLARE @SQL varchar(1000) SET @SQL = 'SELECT ManufacturerName, Model, CAST(EngineSize AS decimal(10,1)) AS [EngineSize], EnginePower, FuelType, DATENAME(Month, DATEADD(Month, DATEPART(Month, Date), 0) - 1) AS [Month], SUM(Price) AS [Total Sales], COUNT(*) AS [Total Orders] FROM [SALES] ' ... SET @SQL = @SQL + 'GROUP BY DATEPART(m, Date), ManufacturerName...' SET @SQL = @SQL + 'ORDER BY ManufacturerName, Model, DATEPART(m, Date)...' EXEC(@SQL) END GO
示例项目定义了一个名为‘Sales Summary’的报告,该报告使用存储过程‘spTest’来返回销售数据。由于我们的存储过程定义了输入参数,因此我们的‘Sales Summary’报告定义了相同的参数,这些参数显示在‘报告筛选器预览’页面上,使用户能够筛选报告结果。
但是,如果数据库存储过程输入参数具有默认值(例如 null,即 @Model VARCHAR(30) = null
),我们可以将其从我们的应用程序报告定义中省略。在这种情况下,该参数不会显示在‘报告筛选器预览’页面上,并且将在存储过程中作为默认值传递。当为我们的报告定义了参数但用户未通过‘报告筛选器预览’页面提供值时,也是如此。
将 RBAC 添加到现有 MVC 应用程序
将 RBAC 框架添加到现有应用程序需要执行第一部分中概述的步骤,然后执行下面概述的步骤:
1. 在 Visual Studio 中使用解决方案资源管理器,在项目中创建一个名为‘Common’的新文件夹,然后通过右键单击新创建的文件夹并选择“添加 >> 现有项…”来添加 |
|
用于 jQuery 的 DataTables 表插件
DataTables 是由 SpryMedia Ltd 免费提供的 jQuery 库的免费开源插件,可在您的应用程序中以任何方式(包括商业项目)使用。有关更多详细信息,请参阅 http://www.datatables.net。它是一个高度灵活的工具,可以为任何 HTML 表格添加高级交互控件。这使得 DataTables 插件成为我们自定义销售表的理想选择,增加了任何报告所需的两项重要功能:列排序和表过滤。
我们现在可以通过 DataTables 插件提供的搜索框过滤我们的表格。只需键入一个值,插件就会过滤结果;然后我们可以按列类型排序。
DataTables 配置
DataTables 只有一个 JavaScript 库依赖项 jquery.dataTables.js
,它使用 CSS 文件 jquery.dataTables.css
中定义的样式。CSS 文件是可选的,但它提供了表的默认样式,使其易于使用。要将 DataTables 集成到您的应用程序中,只需将这两个文件添加到应用程序的 Content 文件夹,如下所示,并在 _Layout.cshtml 文件中相应地引用。
1. 在 Visual Studio 中使用解决方案资源管理器,在 Content 文件夹中创建一个名为‘css’的新文件夹,并通过右键单击新创建的文件夹并选择“添加 >> 现有项…”来添加 |
|
_Layout.cshtml 配置
jquery.dataTables.css
和 jquery.dataTables.js
文件需要在 _Layout.cshtml 文件中引用,如下所示。
_Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RBAC Demo</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
@Scripts.Render("~/bundles/modernizr")
<!-- JQuery References -->
<script src="@Url.Content("~/Scripts/jquery-1.10.2.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery-ui-1.10.3.custom.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery-common.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery-validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery-validate.unobtrusive.js")" type="text/javascript"></script>
<!—DataTables library references -->
<script src="~/Content/js/jquery.dataTables.js"></script>
<link href="~/Content/css/jquery.dataTables.css" rel="stylesheet" />
</head>
<body>
...
注意:如果您想删除 DataTables 插件,只需从您的项目中删除库文件 (x2) 以及 _Layout.cshtml 文件中相应的引用链接(如上图所示)。
初始化 DataTables
为了让 DataTables 能够增强我们的 HTML‘Car Sales’表,我们必须在 customSalesSummary.cshtml
模板中初始化插件,如下所示。
customSalesSummary.cshtml
<script type="text/javascript">
$(document).ready(function () {
$('#ResultsTable').dataTable();
});
</script>
<div>
<table id="ResultsTable" name="ResultsTable" style="width: 100%">
<thead>
@foreach (DynamicDataRow item in Model)
{
foreach (DynamicDataObject col in item.Columns)
{
<th>
@col.Name
</th>
}
break;
}
</thead>
@foreach (DynamicDataRow item in Model)
{
<tr>
@foreach (DynamicDataObject col in item.Columns)
{
<td>
@if (col.DataType == "money")
{
@Convert.ToDecimal(col.Value).ToString("#,##0.00");
}
else
{
@col.Value
}
</td>
}
</tr>
}
</table>
</div>
就是这样!DataTables 将默认添加排序、搜索、分页和信息功能到您的表中,让您的最终用户能够尽快找到他们想要的信息。
通过表格过滤使用自定义 DOM 事件重新计算“总销售额”
DataTables 提供了触发自定义 DOM 事件的能力,这些事件可以被监听并随后进行处理以实现事件驱动的操作。
|
如果需要在使用表记录进行筛选时重新计算“总销售额”标签,我们需要在 DataTables 自定义‘ |
注册回调函数后,每次触发表的‘on draw
’事件时都会收到通知。我们的回调函数将包含更新“总销售额”表单标签的代码。标签将使用根据筛选记录提供的数据重新计算的销售额值进行更新。
以下代码注册了一个回调函数,该函数在 DataTables 自定义‘on draw
’表事件触发时执行。回调函数调用 calculateColumnTotal(index)
函数,该函数返回指定表列的总计。在返回回调函数之前,使用逗号作为“千位”分隔符格式化总计。
<script type="text/javascript"> function calculateColumnTotal(index) { var total = 0; $('table tr').each(function () { var cellvalue = $('td', this).eq(index).text(); var value = Number(cellvalue.replace(/[^0-9\.]+/g, "")); if (!isNaN(value)) { total += value; } }); return total.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,"); } $(document).ready(function () { ... var table = $('#ResultsTable').DataTable(); table.on('draw', function () { $("#SalesTotal").html("<h2>Total Sales £" + calculateColumnTotal(6) + "</h2>"); }); }); </script>
calculateColumnTotal(index)
函数利用 jQuery 返回给定表列的总计。目标列通过传递的列索引(例如,在我们的示例中表示销售数据的列索引 6)来标识。回调函数使用 calculateColumnTotal(index)
函数返回的值更新“总销售额”表单标签。
结论
扩展的框架为任何需要动态独立于其他系统的基于角色的访问控制 (RBAC) 或动态基于角色的报告 (RBR) 的内联网应用程序奠定了理想的基础。
大多数组织在某个时候都需要一个报告设施来报告应用程序数据库中存储的数据,并能够将这些结果导出到逗号分隔值 (CSV) 标准文件格式,这是一种常见的数据交换格式,被消费者、企业和科学应用程序广泛支持。总的来说,并非所有用户都应被允许访问系统内定义的每份报告或导出报告数据的权限。扩展的框架将允许系统管理员控制哪些报告对哪些用户可用(基于用户角色),以及谁有权从报告中导出数据。如果数据被“不可信”用户导出并泄露,您将无法控制您的数据。机密信息的盗窃一直是许多企业的一个大问题。如果企业客户或财务信息落入竞争对手手中,企业及其客户可能会遭受广泛的、不可挽回的经济损失,以及公司声誉的损害。保护机密信息是业务需求,在许多情况下也是道德和法律要求。
该框架是特意设计的,非常灵活,使我们的默认报告解决方案可以替换为其他供应商的解决方案,而不会影响底层的 RBAC 报告功能。基于角色的访问控制 (RBAC) 现在已被大多数组织公认为设置此类控件的最佳实践。
此解决方案特别适用于授予对生产 Web 服务器访问权限有限的企业内联网应用程序。在下一篇也是最后一篇文章中,本文将扩展该框架以涵盖使用 HTTPS 和 OAuth 的 ASP.NET MVC Web 应用程序中的 RBAC。