通用 CRUD
如果你要对实体使用内置的通用列表和编辑页面,请参照本节去实现。
概述
通用 CRUD 是通过 QuickAdmin.Service 类库和 QuickAdmin.RCL 类库配合实现的, QuickAdmin.Service 服务类库内提供了适用不同类型实体的通用泛型基类,QuickAdmin.RCL 类库内则提供了通用 CRUD 配置以及记录列表、记录编辑等相关类型定义和 UI 页面, 并在实体服务与 UI 页面之间设计了一个代理类 CRUDProxy 来实现 UI 对实体服务的调用。
代理存储了通用 CRUD 的元数据,其中包含实体类型以及用来定制操作行为和 UI 界面的各项配置信息。每个代理的别名(TypeAlias)为其唯一标识,即一个别名对应一个代理。 代理可通过 json 配置文件进行无编程定制。
代理通过注册机制加入到系统中,注册好后,即可使用 RCL 类库内置的通用 CRUD 页面:
- 列表页面:
~/QAdmin/Common/EntityCRUD/List/{typeAlias}/{filterable?}/{editable?}
其中 typeAlias 为代理别名,filterable (值为 0 或 1)指示列表是否允许条件过滤(页面是否显示查询相关控件),editable (值为 0 或 1)指示列表中数据是否允许编辑(页面是否显示新增、编辑、删除等相关按钮)。
- 编辑页面:
~/QAdmin/Common/EntityCRUD/Edit/{typeAlias}/{id?}
或
~/QAdmin/Common/EntityCRUD/Edit/{typeAlias}?id={id}
其中 typeAlias 为代理别名,id 为记录主键值。若提供了 id,表示编辑记录,否则表示添加记录。
还可利用编辑页按照录入布局只读展示数据记录,只需追加一个 viewing=1 参数:
~/QAdmin/Common/EntityCRUD/Edit/{typeAlias}?id={id}&viewing=1
- 记录详情页面:
~/QAdmin/Common/EntityCRUD/Details/{typeAlias}?id={id}
其中 typeAlias 为代理别名,id 为记录主键值。
通过配置,内置页面可以和自己的页面自由组合使用,例如列表页用通用 CRUD 的,编辑页面用自己的,或者列表页用自己的,编辑页用通用 CRUD 的。
样例
先来看看具体实现效果,生成一个登录日志(即 SysLoginLog 实体)的查询页面。
用超级管理员用户 admin 登录示例项目或在线演示,找到并打开 已注册CRUDProxy 页面,点击 输入并注册 按钮,TypeAlias 输入 "LogSample", json 配置输入以下内容进行注册:
{
"EntityType": "QuickAdmin.Entity.SysLoginLog",
"DataListProperties": "UserLoginId, UserName, DeptPath, UserIp, LoginTime, State, Device, OS, Browser, Remarks",
"GridControl": {
"PopupDetailsOnRowDbClick": true,
"ShowQuickFilterContextMenu": true
}
}
然后点击列表里刚注册的 LogSample 链接,即可打开生成的查询页面,见下图,自动生成了各种查询条件:部门树,登录时间,状态枚举以及其它属性条件。 列表里自动填充了用户所属部门的名称,状态枚举也被自动"翻译"为其名称而不是显示其 int 值。
json 配置里 EntityType 是必须提供的配置项,指示了实体类型。 DataListProperties 配置指示了列表显示哪些属性。 PopupDetailsOnRowDbClick 配置指示双击表格行时,将打开一个记录详情页面(默认为一个内置页面,可配置为其它页面)。 ShowQuickFilterContextMenu 配置指示鼠标右击单元格时,弹出一个上下文菜单,可快速用单元格内容检索。
再生成一个示例里的 风险单元 实体的增删改查页面。TypeAlias 输入 "FxdySample",json 配置输入以下内容进行注册:
{
"EntityType": "QAdminDempApp1.Entity.Samples.Fxdy"
}
EntityType 的 namespace 部分需要改为当前项目的 namespace。
点击列表里刚注册的 FxdySample 链接,即打开生成的增删改查页面,见下图。
查询条件默认支持输入比较运算符,比如下图输入了 ">0",表示查询风险等级大于 0 的记录,参见 AllowFilterValueStartsWithOperator/ParseFilterItemValue。
实现步骤
要对一个实体自动构建增删改查页面,实现步骤如下:
- 将实体类型定义为 ICRUDEntity 类型,其对应过滤器类型则需继承自 CommonFilter(或直接就用 CommonFilter)
- 实现一个继承自 CommonCRUDService<> 泛型基类或其子类的实体服务(可选)
- 实现一个与实体类型对应的 CRUDProxy 代理类(可选)
- 创建实体对应的代理的 json 配置文件(可选)
- 在应用启动时注册创建的代理(可选)
- 将实体对应通用 CRUD 页面加入系统菜单,供用户使用
即只需定义好实体类型,无需再编写任何代码就可直接使用通用 CRUD,将该实体类型的全名作为 typeAlias 传给通用页面即可:
namespace MyApp.Entity
{
[Table(Name = "biz_foo")]
public class FooEntity : IEntityWithAutoIdKey, ICRUDEntity
{
// 加入各个业务字段
}
}
打开通用列表页面即可进行分页查询以及增删改等操作,并会自动记录操作日志:
~/QAdmin/Common/EntityCRUD/List/MyApp.Entity.FooEntity/1/1
以下为详细步骤。
实体类型定义
实体类型(TEntity)需要定义为 ICRUDEntity<TKey> 或 IReadOnlyCRUDEntity<TKey>, 区别是前者表示可对实体进行增、删、改、查操作,后者表示只能对实体进行查询操作。这两个接口内并无需要实现的任何成员,只起一个标识作用。
如果要定义实体过滤器类型(TFilter),则该类型必须继承自 CommonFilter 或其子类型。 内置通用 CRUD 功能默认直接使用 CommonFilter 作为过滤器类型,自定义实体过滤器类型不是必须的。
// 例如系统登录日志实体类型是这样定义的
public class SysLoginLog : EntityWithAutoIdKey, IDeptRelatedEntityWithDeptPath, IReadOnlyCRUDEntity, ITruncateColumnValues<SysLoginLog>
{
...
}
public class LoginLogFilter : DeptRelatedDataFilter
{
...
}
// 示例里风险单元实体类型是这样定义的
public class Fxdy : FullAuditEntityWithAutoIdKey, IDeptRelatedEntityWithDeptPath, IEntityWithName, ICRUDEntity
{
...
}
public class FxdyFilter : DeptUserRelatedDataFilter
{
...
}
实体服务
根据实体类型的不同,实体服务需要定义为继承自 CommonCRUDService<> 或其子类的类。
通常如果对实体除过增删改查并无其他操作,则无需实现实体服务,系统会根据实体的类型,自动使用内置的泛型基类作为实体服务进行增删改查操作:
- CommonCRUDService/SortableCommonCRUDService
- TreeEntityCRUDService/SortableTreeEntityCRUDService
- DeptRelatedCRUDService/SortableDeptRelatedCRUDService
例如对于 ISortableEntity,可能会使用 SortableCommonCRUDService<>, 对于 IDeptRelatedEntity,可能会使用 DeptRelatedCRUDService<>。
并且,系统内置发现机制,能够自动找到并使用自行实现的对应实体服务,比如自行实现了 风险单元 实体服务:
public interface IFxdyService : ICRUDService<Fxdy, long, Fxdy, FxdyFilter>, IDeptRelatedPagingService
{
...
}
public class FxdyService : DeptRelatedCRUDService<Fxdy, long, Fxdy, FxdyFilter>, IFxdyService
{
...
}
系统将能自动找到 FxdyService 作为 风险单元 实体的通用 CRUD 实体服务。
实体对应代理类
若要对实体使用自定义的代理类,可按如下继承一下 CRUDProxy 并指定为 ICRUDProxy<TEntity> 即可:
public class MyEntityCRUDProxy : CRUDProxy, ICRUDProxy<MyEntity>
{
...
}
当要对实体进行自定义操作时:例如要在列表页面加入自定义按钮,并响应用户的点击操作,此时需要自定义代理类,并在其中重写 OnListPageCustomCommand 方法。
创建配置文件
可通过 json 文件对代理进行配置,指定操作行为,定制界面元素等。可配置项目参见 CRUDProxy.ProxyConfiguration 类文档。
为代理起个别名,比如 QuickAdmin.Entity.SysLoginLog 实体代理别名可命名为 "LoginLog",QAdminDempApp1.Entity.Samples.Fxdy 实体代理别名可命名为 "Fxdy", 每个代理对应的 json 配置文件的名称即是 "{别名.json}",其存储位置默认为应用程序根下的 configs 目录下的 crudProxy 目录内。
注册代理
在应用启动时,可通过 CRUDProxy 类提供的几组静态方法去手动注册代理:RegisterProxy/RegisterProxyByConfiguration/RegisterProxiesInEmbeddedResource 等。
若 AutoRegisterCRUDProxies 为 true,系统将在应用程序启动时自动注册 configs/crudProxy 目录及其子目录下的所有 .json 文件, 因此你可将配置放到该目录下,让系统自动注册。
注册操作也不是必须的,系统支持即用即注册,即当访问一个通用 CRUD 页面时(该页面 Url 里会包含别名参数),系统会检查别名对应的代理是否已注册,若没有会尝试先去注册: 若 configs/crudProxy 下存在 "{别名.json}",将加载该文件进行注册;若文件不存在且别名是一个实体类型的全称,将用默认配置注册该实体的代理。
加入系统菜单
进入 系统菜单 页,将通用 CRUD 列表页加入系统菜单指定位置,供用户使用,如:
~/QAdmin/Common/EntityCRUD/List/Fxdy/1/1
代理的维护
系统内置了代理的配置生成、注册、注销功能,可方便地进行生成、测试与部署。
生成CRUDProxy配置 (~/QAdmin/Common/EntityCRUD/GenCfgJson) 页里列出了所有 ICRUDEntity 和 IReadOnlyCRUDEntity 类型实体(下拉框内以 (R) 结尾的实体是 IReadOnlyCRUDEntity 类型实体),并可生成各自默认的代理配置。
在 已注册CRUDProxy (~/QAdmin/Common/EntityCRUD/RegisteredProxies) 页面里,可进行代理的注册、注销,查看、下载当前配置等。对于从 json 文件加载的配置,还可进行重新加载、下载配置文件等操作。
代理可以用目录结构进行组织以便于维护,比如 Dics 目录下放置所有码表数据表的代理配置,Bizs 目录下放置业务数据表的代理配置,这些都在此页面里可实现。
同一个实体可注册多个不同别名的代理,供不同的用户组里的用户使用。比如对于一个业务实体,某些用户可以进行增删改查,另一些用户则只允许查询数据,此时可通过设计两个代理去实现此需求。
配置说明
代理在注册时会依据实体具体类型初始化各个属性配置,即 CRUDProxy 里的各个属性会在注册时自动初始化好, 如果发现有对应的 json 配置文件,则会用配置文件里的有效配置覆盖先前初始化好的配置,因此你只需在 json 配置文件里书写需要更正的配置项。
你可到 生成CRUDProxy配置 页里去查看生成的默认配置以及生成的列表页和编辑页,若需要调整配置,再去到 已注册CRUDProxy 页面里进行更改、测试配置。
json 文件里的可配置项目参见 CRUDProxy.ProxyConfiguration 类文档, 其中 EntityType 是唯一一个必须配置的项目,其它都是可选的。 对于一些简单的数据表,比如只有主键+名称的码表,通常无须为其创建 json 配置文件。
可通过配置实现一些较为复杂的功能,比如示例里的 风险单元 录入页面里的 责任人 选取功能:点击 责任人 下拉框 Trigger 图标,弹出窗口去选人, 就是通过设置 InputControlSettings.Listeners 配置并配合客户端 js 代码实现的。
再如 风险单元 列表页面里的 风险等级 列呈现的四色图(红、橙、黄、蓝四种颜色分别标示不同风险等级,灰色表示尚未评估), 是通过设置该列对应属性的 RendererFunction 并配合客户端 js 代码实现的:
将在后续推出一些章节介绍各种高级用法。
实体特性
如在上图所看到的,列表页面里(包括在编辑等其它页面里)相关标题以及标签均显示为中文了,这是通过设置各对象/属性的 DisplayAttribute 特性实现的。即在实体定义时利用 DisplayAttribute 指定显示名称,通用 CRUD 会在展示该对象/属性的标题或标签的地方自动使用该名称。 也可通过 CRUD 配置里的各属性的 Title 配置来重新指定显示名称。
通用 CRUD 支持若干相关特性例如 Display/Required/StringLength/MinLength/MaxLength/RegularExpression/Range/DataType 等,可用来设置属性的值类型、验证模式、取值范围等等, 通用编辑页面和通用列表页面将按照这些设置自动使用对应的输入控件、验证方法、列表格式等等。
using System.ComponentModel.DataAnnotations;
using QuickAdmin.Entity;
[Table(Name = "test_crud")] // 这是 FreeSql 的 Table 特性,指定表名称
[Display(Name = "测试数据")]
public class TestCrud : AuditEntityWithAutoIdKey, ICRUDEntity
{
// 类别 Id
// 下侧有导航设置,即 CategoryId(外键) 取自 TestCrudCategory 对应数据表主键 Id
// 在录入界面将自动显示为下拉框,其内项目将自动用表 test_crud_category 记录填充
// 在列表页面,则自动成为一个筛选条件下拉框
[Display(Name = "所属类别Id")]
public long CategoryId { get; set; }
[Display(Name = "所属类别")]
[Navigate(nameof(CategoryId))]
public TestCrudCategory Category { get; set; }
[Display(Name = "字符串1")]
[StringLength(30, ErrorMessage = "{0} 最多输入 {1} 个字符")]
[RegularExpression("^(?:[0-9]+[a-zA-Z]|[a-zA-Z]+[0-9])[a-zA-Z0-9]*$", ErrorMessage = "{0} 至少要包含一个字母和数字")]
public string StrCol1 { get; set; }
[Display(Name = "字符串2")]
[Required(ErrorMessage = "{0} 必须输入")]
[MaxLength(20, ErrorMessage = "{0} 最多输入 {1} 个字符")]
[MinLength(5, ErrorMessage = "{0} 最少输入 {1} 个字符")]
public string StrCol2 { get; set; }
// 整型在通用 CRUD 里默认不可输入负值,最小值为 0;最大值不限。可通过 Range 特性自行设置
[Range(1, 100)]
//[Range(typeof(int), "", "100")] // 这样设置(即最小值给了空白字符串)表示 IntCol1 无最小值限制
public int IntCol1 { get; set; } = 1; // 不会为空的值类型,对应字段必然 not null,无需设置 [Required]
[Column(Precision = 5, Scale = 2)]
[Range(1, 100)]
public decimal DecimalCol1 { get; set; } = 1.23m;
[DataType(DataType.Date)] // 录入以及列表里将自动为 年月日
[Range(typeof(DateTime), "2020-01-01", "today")] // 录入时的可选范围是 2020-01-01 至 今天
public DateTime DateCol1 { get; set; } = EntityHelper.NowForEntity;
[DataType(DataType.DateTime)] // 录入以及列表里将自动为 年月日时分秒
[Range(typeof(DateTime), "thisMonth", "")] // 录入时的可选最小日期是当前月份的第一天,最大日期无限制
public DateTime? DateCol2 { get; set; } = EntityHelper.NowForEntity;
// 非空枚举类型,在录入时将默认自动使用下拉框,下拉框里自动用枚举名称列表填充
public LoginWay EnumCol1 { get; set; } = LoginWay.IdAndPwd;
// 可为空枚举类型,在录入时将默认自动使用可编辑下拉框(能清除输入)
public OperationLogOPType? EnumCol2 { get; set; }
// 非空 bool 类型,在录入时将默认自动使用 CheckBox
[Column(MapType = typeof(int))]
public bool BoolCol1 { get; set; }
// 可为空 bool 类型,在录入时将默认自动使用可编辑下拉框(能清除输入)
[Column(MapType = typeof(int?))]
public bool? BoolCol2 { get; set; }
// StringLength <0 或 >= 1024 录入时将默认自动使用多行文本框
[StringLength(1024)]
public string Memo1 { get; set; }
[StringLength(1200)]
[DataType(DataType.Text)] // 长度 >= 1024 但要使用单行文本框,可这样设置
public string Memo2 { get; set; }
[StringLength(500)]
[DataType(DataType.MultilineText)] // 长度 < 1024 但要使用多行文本框,可这样设置
public string Memo3 { get; set; }
}
[Table(Name = "test_crud_category")]
[Display(Name = "测试数据类别")]
public class TestCrudCategory : SortableAuditEntityWithAutoIdKey, ICRUDEntity
{
[Required]
public string Name { get; set; }
}
有关最小值、最大值设置参考 MinValue/MaxValue。
在代理注册时,将按照这些特性初始化各个相关设置。可通过 json 配置文件或代码进行重新指定。