CRUD
TsgcHTMLComponent_CRUD:一张表的列表、搜索、分页、创建与编辑表单、删除确认和写入路径,集于一个组件,基于 TDataSet 或您自己的 SQL,适用于 Delphi、C++ Builder 和 .NET。
TsgcHTMLComponent_CRUD:一张表的列表、搜索、分页、创建与编辑表单、删除确认和写入路径,集于一个组件,基于 TDataSet 或您自己的 SQL,适用于 Delphi、C++ Builder 和 .NET。
每个业务应用为每张表都要重写一遍的那个页面,现在是一个组件。给它一个键字段和一个路由前缀,然后绑定 TDataSet,或者交给它一个基于您自己 SQL 的六方法适配器。ShowList 返回标记,页面回传的内容则通过 ProcessAction 处理。
TsgcHTMLComponent_CRUD,以及同一单元中的 TsgcHTMLCRUDAdapter 和 TsgcHTMLCRUDAdapter_DataSet
带搜索和分页器的列表、创建与编辑表单,以及删除确认
Delphi, C++ Builder, .NET
设置 KeyField 和 RoutePrefix,绑定一个处于活动状态的 DataSet,然后调用 ShowList(search, page)。如果使用您自己的 SQL,则改用 SetAdapter 为组件提供一个适配器。页面回传的内容通过 ProcessAction 路由。
uses
Classes, sgcHTML_Component_CRUD;
var
oCRUD: TsgcHTMLComponent_CRUD;
vHTML: string;
begin
oCRUD := TsgcHTMLComponent_CRUD.Create(nil);
try
oCRUD.CRUDID := 'customers';
oCRUD.Title := 'Customers';
oCRUD.RoutePrefix := '/customers';
oCRUD.KeyField := 'ID';
oCRUD.PageSize := 15;
oCRUD.Actions := [caCreate, caEdit, caDelete, caView, caExport];
oCRUD.VersionField := 'UPDATED_AT'; // optimistic locking
oCRUD.OnValidate := CustomersValidate;
// an active dataset: the columns are read off it
oCRUD.DataSet := qryCustomers;
vHTML := oCRUD.ShowList('', 1); // search text, page number
finally
oCRUD.Free;
end;
end;
// The rules that are about your data. Add a line per problem: nothing is
// written and the form comes back with the messages.
procedure TServer.CustomersValidate(Sender: TObject;
aOperation: TsgcHTMLCRUDOperation; const aKey: string;
aValues, aErrors: TStrings);
begin
if Length(Trim(aValues.Values['NAME'])) < 3 then
aErrors.Add('The name needs at least three characters.');
end;
// A message from the browser carries action, crud and key with the form fields
if oCRUD.ProcessAction(vAction, vKey, oFields, vHTML) then
aResponse := vHTML; // the markup to swap into the page
// Or over your own SQL: an adapter of six methods
type
TCustomersAdapter = class(TsgcHTMLCRUDAdapter)
public
function Locate(const aKey: string): Boolean; override;
function Read(const aKey: string; aValues: TStrings): Boolean; override;
function List(const aSearch: string; aPage, aPageSize: Integer;
aRows: TStrings): Integer; override;
function Insert(aValues: TStrings; var aKey: string;
aErrors: TStrings): Boolean; override;
function Update(const aKey: string; aValues, aErrors: TStrings): Boolean;
override;
function Delete(const aKey: string; aErrors: TStrings): Boolean; override;
end;
oCRUD.SetAdapter(TCustomersAdapter.Create(oCRUD)); // the component owns it
// includes: sgcHTML_Component_CRUD.hpp
TsgcHTMLComponent_CRUD *oCRUD = new TsgcHTMLComponent_CRUD(NULL);
try
{
oCRUD->CRUDID = "customers";
oCRUD->Title = "Customers";
oCRUD->RoutePrefix = "/customers";
oCRUD->KeyField = "ID";
oCRUD->PageSize = 15;
oCRUD->Actions = TsgcHTMLCRUDActions() << caCreate << caEdit << caDelete
<< caView << caExport;
oCRUD->VersionField = "UPDATED_AT"; // optimistic locking
oCRUD->OnValidate = CustomersValidate;
// an active dataset: the columns are read off it
oCRUD->DataSet = qryCustomers;
String html = oCRUD->ShowList("", 1); // search text, page number
}
__finally
{
delete oCRUD;
}
// The rules that are about your data. Add a line per problem: nothing is
// written and the form comes back with the messages.
void __fastcall TServer::CustomersValidate(TObject *Sender,
TsgcHTMLCRUDOperation aOperation, const String aKey,
TStrings *aValues, TStrings *aErrors)
{
if (aValues->Values["NAME"].Trim().Length() < 3)
aErrors->Add("The name needs at least three characters.");
}
// A message from the browser carries action, crud and key with the form fields
String html;
if (oCRUD->ProcessAction(action, key, fields, html))
response = html; // the markup to swap into the page
// For your own SQL, derive from TsgcHTMLCRUDAdapter, override its six methods
// (Locate, Read, List, Insert, Update, Delete) and call oCRUD->SetAdapter().
using System.Collections.Generic;
using esegece.sgcWebSockets;
var crud = new TsgcHTMLComponent_CRUD();
crud.CRUDID = "customers";
crud.Title = "Customers";
crud.RoutePrefix = "/customers";
crud.KeyField = "ID";
crud.PageSize = 15;
crud.Actions = TsgcHTMLCRUDActions.caCreate | TsgcHTMLCRUDActions.caEdit
| TsgcHTMLCRUDActions.caDelete | TsgcHTMLCRUDActions.caView
| TsgcHTMLCRUDActions.caExport;
crud.VersionField = "UPDATED_AT"; // optimistic locking
// The key travels with the page: it is in neither the list nor the form
var id = crud.Columns.Add();
id.FieldName = "ID";
id.InList = false;
id.InForm = false;
var name = crud.Columns.Add();
name.FieldName = "NAME";
name.Caption = "Name";
name.Required = true;
name.MaxLength = 120;
// There is no TDataSet: read a DataTable, an IList<T> or a table behind a DbConnection
crud.SetAdapter(new TsgcHTMLCRUDAdapter_DataTable(crud, customersTable));
// The rules that are about your data
crud.OnValidate += (object sender, TsgcHTMLCRUDOperation op, string recordKey,
List<string> values, List<string> errors) =>
{
if (TsgcHTMLComponent_CRUD.ValueOf(values, "NAME").Trim().Length < 3)
errors.Add("The name needs at least three characters.");
};
string html = crud.ShowList("", 1); // search text, page number
// A message from the browser carries action, crud and key with the form fields
if (crud.ProcessAction(action, key, fields, out string reply))
response = reply; // the markup to swap into the page
您最常使用的成员。
TsgcHTMLCRUDAdapter 是写入路径,有六个虚方法:Locate、Read、List、Insert、Update 和 Delete。TsgcHTMLCRUDAdapter_DataSet 驱动绑定的数据集,SetAdapter 则把您自己的适配器交给组件,并由组件接管其所有权。List 以制表符分隔的行返回一页数据,键放在最后一个单元格,并返回整个数据集共有多少行。
.NET 中没有 TDataSet,因此移植版提供了三个适配器:TsgcHTMLCRUDAdapter_DataTable(基于 DataTable)、TsgcHTMLCRUDAdapter_List<T>(基于任意 IList<T>)和 TsgcHTMLCRUDAdapter_DbConnection(基于 DbConnection 背后的表),每个值都作为参数发送。请声明 Columns:它们不会从表中读取。
ShowList(search, page)、ShowNew、ShowEdit(key)、Save(key, values, html) 和 DeleteRecord(key, html) 各自返回宿主换入页面的标记。ProcessAction 把回传的 crudList、crudNew、crudEdit、crudSave 和 crudDelete 分别路由给它们,对任何其他操作则返回 False,因此一个页面可以容纳多个组件。
Actions 是由 caCreate、caEdit、caDelete、caView 和 caExport 组成的集合,只有 caView 的页面就是一份报表。EditMode 为 cemModal 或 cemPage,ConfirmDelete 在删除前进行询问,PageSize 默认为 25,最大不超过 500,SearchFields 列出搜索框查找的字段,以分号分隔。
TsgcHTMLCRUDColumn 有 FieldName、Caption、Visible、ReadOnly、Required、InList、InForm、Width 和 MaxLength。如果没有声明任何列,组件会从活动数据集中读取它们:标题取自 DisplayLabel,还有必填标志和长度,键会显示但从不编辑。LoadColumnsFromDataSet 可按需执行此操作。
首先会拒绝为空的必填列,以及长度超过 MaxLength 的值。然后触发 OnValidate:为每个问题向 aErrors 添加一行,就不会写入任何内容,表单会带着这些消息返回。OnApplyUpdates 在适配器之前运行,设置 aHandled 会让写入由您来完成。OnApplied 在写入完成之后触发,用于记录日志,或刷新正在查看该列表的其他人。
在 VersionField 中指定一个版本字段。保存时会把浏览器发送的内容与已存储的内容进行比较,如果期间有其他人修改了该记录,就拒绝写入,因此第二个点击 Save 的人不会覆盖第一个人的修改。
Details 保存记录的子列表。每个条目指定另一个 TsgcHTMLComponent_CRUD(放在 CRUD 中)、子表中保存主键的字段(放在 MasterField 中)和一个 Caption。编辑记录时,它们会显示在表单下方。SetMaster(field, value) 把列表限定到一条主记录。
有了 caExport,工具栏会显示一个指向 RoutePath('export.csv') 的 Export 链接。用 GetExportCSV(search) 响应该路由:它导出列表当前显示的搜索结果,最多 500 行,而不是整张表。每个值都带引号,因此逗号或换行不会破坏文件。
每个键都来自浏览器。ShowEdit、Save 和 DeleteRecord 在编辑或删除任何内容之前,都会要求适配器对它执行 Locate,因此更改 URL 中的数字不会触及另一条记录。Actions 同样会强制执行:没有 caDelete 的页面,即使伪造的表单提交了删除请求也会拒绝。单元格和字段都经过转义,因此包含标记的记录会显示为文本。
搜索框、New、Edit、Delete 以及编辑表单本身,都是标有 data-sgc-ws-send 的表单,每个表单带有隐藏的 action、crud 和 key 字段,宿主把它们交给 ProcessAction。通过 socket 发送的消息不带路径,也永远不会到达路由器,因此请自行进行授权。在 Delphi 中,TsgcHTMX_Engine_Server.MessageSession 返回握手 cookie 所对应的会话,或者返回 nil。对于 HTTP,RoutePath 会构建宿主在引擎路由器上声明的路径:/customers、/customers/new、/customers/{id}/edit、/customers/save 和 /customers/{id}/delete。
可以像上面那样在代码中创建它,也可以使用 Delphi IDE 中的向导:Tools › New sgcHTML CRUD page 会列出当前打开的窗体上的数据集,读取您选中的数据集的字段,并向窗体添加一个已配置好的组件,同时把两行连接代码放到剪贴板中。它需要 Delphi 10.4 或更高版本。在 IDE 之外,可以使用 sgcHTMLGen 命令行工具,根据表、路由前缀和字段,生成该页面的 Object Pascal 或 C# 源代码,每个字段写成 name:flag,例如 ID:key 或 NAME:req:60。
属于 sgcHTML,后者独立于 sgcWebSockets 单独销售。该单元在定义了 SGC_HTML 的地方编译,sgcVer.inc 会在除 Android 和 iOS 之外的所有平台上为 HTML 包定义它。演示位于 Demos\60.HTML\01.RunTime\02.AdminCRUD,其中客户区域基于此组件构建,旁边是它所取代的手写页面。