Core Docs

Feature / Section / Component (cấu hình UI từ database)

3 bảng DB quyết định mọi màn hình. Bạn config ở đây — Core tự render.

Đây là cốt lõi của Core low-code. Hiểu được mục này = hiểu cách dùng Core.

Mô hình 3 lớp

Feature                           ← 1 màn hình logic (vd "Khách hàng")
  └── ComponentGroup              ← 1 vùng layout (vd "Header", "Body", "Tab thông tin")
        └── Component             ← 1 widget cụ thể (Textbox, Number, GridView, Button…)

Mỗi feature có nhiều section. Mỗi section chứa nhiều component. Section có thể lồng nhau (qua ParentId).

Bảng Feature

1 row = 1 màn hình. Field bạn thường set:

FieldMô tả
NameĐịnh danh trong code (khớp nameof(...) khi viết class C#).
LabelTiêu đề hiển thị trên tab/popup.
EntityNameEntity backend tương ứng (vd Customer).
IconIcon path (cho menu + tab header).
ActiveBật/tắt feature.
IgnoreEncodeTrue → input string không URL-encode 6 ký tự đặc biệt (+ / ? # & ') khi save. Default false (encode). Xem String encoded vs raw.

Trong code, : base(nameof(Customer)) của TabEditor/PopupEditor sẽ tìm row Feature có Name = "Customer".

Bảng ComponentGroup (Section)

1 row = 1 vùng layout. Field bạn thường set:

FieldMô tả
NameTên section trong feature.
FeatureIdFeature sở hữu.
ParentIdSection cha (cây — null nếu là gốc).
ElementTypeTag HTML (div, td, fieldset, …).
ClassNameClass CSS gắn lên element.
OrderThứ tự giữa siblings.
ActiveBật/tắt section.
Column/XsCol/SmCol/LgCol/XlColSố cột grid theo screen width — chia cột responsive không cần viết CSS.
VisibilityExpression evaluate trên entity → ẩn/hiện theo record.

→ Cách dùng *Col để chia layout responsive: Section & chia cột.

Bảng Component (Widget)

1 row = 1 widget (input, button, grid, …). Field cấu hình tùy theo ComType.

Field cốt lõi (mọi widget đều cần)

FieldMô tả
NameĐịnh danh trong feature (dùng để FindComponentByName<T>("Name")).
ComponentGroupIdSection chứa widget này.
ComTypeTên class renderer (vd Textbox, Number, GridView, Button…).
FieldNameProperty của entity để bind (hỗ trợ dotted path: "Customer.Address.City").
LabelLabel hiển thị.
ShowLabelTrue → render <label> cạnh widget.
OrderThứ tự render trong section.
ActiveBật/tắt.
EditableFalse → render readonly.
VisibilityExpression ẩn/hiện theo record.
ColumnSpan bao nhiêu ô trong grid section (default 2).

Field tùy ComType

Mỗi ComType đọc 1 tập field khác nhau. Đặt field component không đọc cũng vô tác dụng.

Tham chiếu component liệt kê chính xác mỗi ComType dùng những field nào.

Ví dụ:

  • ComType = "Textbox" đọc thêm: UpperCase, FormatData, …
  • ComType = "Number" đọc thêm: Precision.
  • ComType = "GridView" đọc thêm: Reference, DataSourceFilter, CanAdd, CanSearch, IsRealtime, …
  • ComType = "Dropdown" (alias) đọc thêm: Reference, RefName, DataSourceFilter, Template, …

Trong code Core, field này thường gọi là ComponentType (Component.ComponentType). DB column cũng tên ComponentType. Cùng 1 field.

ComponentType — alias mapping (ComponentFactory.GetComponent)

Field ComponentType không phải tên class trực tiếp — nó là alias được ComponentFactory.GetComponent(ui, form, canWrite) map sang class theo logic switch. Nhiều giá trị có variant tự pick.

Bảng sau từ source Core/Components/Extensions/ComponentFactory.cs:

ComponentTypeClass C# instantiateGhi chú
(rỗng)LabelCellTextDefault khi không set.
LabelCellTextHiển thị text formatted (read-only).
InputTextboxAlias cho Textbox.
PasswordTextbox với Password = trueTextbox kiểu password.
TextareaTextarea
AutocompleteTextboxAutocompleteTextbox
TimepickerTimepicker
PdfPdf
DocumentWriteDocumentWrite
LinkLink
ImageImageUploader (với CanWrite set)Alias cho ImageUploader.
DropdownSearchEntry nếu Precision < 2, MultipleSearchEntry nếu Precision >= 2Alias đa dụng cho picker. Dùng Precision để switch single/multi-select.
DropdownStringSearchEntryStringAlias — bind bằng string code thay vì id.
GridViewGridView nếu GroupBy rỗng, GroupGridView nếu setTự pick group variant dựa GroupBy.
VirtualGridVirtualGrid nếu màn hình ≥ 768px; nhỏ hơn fallback GridView / GroupGridViewTự fallback theo screen width.
ListViewListView nếu GroupBy rỗng, GroupListView nếu setTự pick group variant.
KhácReflection: Type.GetType("Core.Components." + ComponentType)Vd "Datepicker"Core.Components.Datepicker. Bất kỳ class kế thừa EditableComponent trong namespace Core.Components đều dùng được.

Vài lưu ý khi cấu hình:

  • Cell grid hiển thị dropdown pickerComponentType = "Dropdown" + RefName + FormatCell. Đừng dùng "SearchEntry" trực tiếp — Dropdown là alias chuẩn.
  • Multi-select → ComponentType = "Dropdown" + Precision >= 2.
  • Cell text read-only formatted ({Customer.Name}) → ComponentType = "Label" (hoặc rỗng).
  • Group rows → set GroupBy = "<FieldName>", giữ ComponentType = "GridView" hoặc "ListView" — Core auto pick GroupGridView/GroupListView.
  • Data rất lớn (>1000 row) → ComponentType = "VirtualGrid" (Core tự fallback GridView trên mobile).
  • Form input password → ComponentType = "Password" (alias).
  • Custom widget bạn tự viết (vd MyChart extends EditableComponent trong Core.Components) → ComponentType = "MyChart", Core tìm qua reflection.

Composited ComponentType — nhiều widget trên 1 cell

Khi ComponentType chứa ký tự không chữ cái (vd -, /, :), Core hiểu là composited:

  • Tách ComponentType theo non-alpha chars → list sub-type.
  • Tách FieldName theo , → list field names.
  • Số sub-type phải = số field; ký tự non-alpha sẽ chèn giữa các widget.

Ví dụ:

# 2 textbox phân cách bởi "-"
ComType:    Input-Input
FieldName:  Code,Name
# Render: <Textbox Code> - <Textbox Name>

# 2 datepicker range, phân cách bởi "/"
ComType:    Datepicker/Datepicker
FieldName:  StartDate,EndDate
# Render: <Datepicker StartDate> / <Datepicker EndDate>

Cả 2 widget đều copy chung GuiInfo (Component cha) nhưng có FieldName riêng → 2 widget bind 2 field khác nhau.

DataSourceFilter — cú pháp OData

Field này là chuỗi OData querystring đầy đủ, bắt buộc bắt đầu bằng ? (vd ?$filter=Active eq true). Core ghép trực tiếp vào URL API khi load data cho widget (GridView, Select2, SearchEntry, …).

# ✅ Đúng — đủ "?$filter=":
DataSourceFilter: ?$filter=Active eq true

# ✅ Nhiều điều kiện AND/OR:
DataSourceFilter: ?$filter=Active eq true and CountryId eq 1

# ✅ Có thêm $expand / $orderby / $top:
DataSourceFilter: ?$expand=Address&$filter=Active eq true&$orderby=Name&$top=200

# ❌ Sai — thiếu "?$filter=", Core sẽ không nhận:
DataSourceFilter: Active eq true

OData operator hay dùng:

Cú phápÝ nghĩa
eq, nebằng / khác
gt, ge, lt, lelớn hơn / lớn-bằng / nhỏ hơn / nhỏ-bằng
and, or, notlogic
contains(Name,'foo')string contains
startswith(Code,'AB')string startsWith
Status/Code eq 'OPEN'navigate qua FK
nullso sánh với null: RegionId eq null

Substitution động — khi cần filter theo entity hiện tại, dùng placeholder {<FieldName>} (curly braces):

DataSourceFilter: ?$filter=CustomerId eq {CustomerId}

Runtime sẽ thay {CustomerId} bằng entity.CustomerId lúc gọi API. Hữu ích cho picker cascade (chọn Customer trước → list Address chỉ load của Customer đó).

Có thể dùng nhiều placeholder + dotted path:

DataSourceFilter: ?$filter=CountryId eq {Customer.CountryId} and Active eq true

Xem thêm cú pháp đầy đủ tại Client (REST + OData).

FormatData / FormatEntity / FormatCell / FormatRow — template string format

Engine duy nhất: Utils.FormatEntity(template, source) parse placeholder {<expr>:<format>} và replace bằng giá trị từ source.

Quan trọng: GridPolicy (cột grid) lưu format ở field FormatCell / FormatRow. Khi render, GridPolicy.MapToComponent() map 1:1 sang Component:

res.FormatData   = header.FormatCell;   // FormatCell → FormatData
res.FormatEntity = header.FormatRow;    // FormatRow  → FormatEntity

Sau đó Utils.GetCellText(component, ...) dùng FormatData/FormatEntity y hệt component bình thường. Cú pháp giống hệt cho cả 4 field.

FieldBảngCú pháp tương đươngÁp dụng vào
FormatDataComponentFormatDataValue đơn (Textbox, Number, Datepicker) hoặc ref entity (SearchEntry, Dropdown).
FormatEntityComponentFormatEntityToàn bộ entity đang bound.
FormatCellGridPolicy= FormatData(Mapped) cell value của row trong grid.
FormatRowGridPolicy= FormatEntity(Mapped) row entity trong grid.

Cú pháp placeholder (chuẩn Utils.FormatEntity)

{<FieldName>}              → entity.FieldName
{<FieldName>:<formatSpec>} → entity.FieldName được format với .NET format string
{<Path.To.Field>}          → dotted path navigate qua FK
{<a * b>}                  → biểu thức tính toán (chỉ khi calc=true, vd template Word)

formatSpecchuẩn .NET format string:

FormatÝ nghĩaVí dụ output
N0Số nguyên có dấu phẩy phân nhóm1,234,567
N2Số 2 chữ số thập phân1,234.57
N4Số 4 chữ số thập phân1,234.5678
dd/MM/yyyyDate Việt Nam15/03/2026
dd/MM/yyyy HH:mmDate + giờ phút15/03/2026 14:30
MM/yyyyTháng/năm03/2026
C0Currency$1,234

Ví dụ FormatEntity (placeholder theo tên field)

# SearchEntry — sau khi pick Customer, hiển thị "ACME — Khách Sài Gòn, TP.HCM"
ComType:       SearchEntry
FieldName:     CustomerId
Reference:     Customer
FormatEntity:  "{Code} — {Name}, {City}"

# CellText hiển thị nested FK
ComType:       CellText
FieldName:     CustomerId
FormatEntity:  "{Customer.Code} ({Customer.Phone})"

# Số hoá đơn + ngày
ComType:       CellText
FormatEntity:  "{InvoiceNo} ngày {InvoiceDate:dd/MM/yyyy}"

Ví dụ FormatData

# Datepicker (special case — Datepicker tự strip {0: và } để lấy format string)
ComType:    Datepicker
FieldName:  InvoiceDate
FormatData: "{0:dd/MM/yyyy}"     # → "15/03/2026"

# Number — format theo field name
ComType:    Number
FieldName:  Amount
FormatData: "{Amount:N2}"        # → "12,345.67"

# SearchEntry — FormatData applied lên matched entity
ComType:       SearchEntry
FieldName:     ProductId
Reference:     Product
FormatData:    "{Code}"          # chỉ hiển thị Code sau khi pick

Lưu ý: Datepicker là trường hợp đặc biệt — code parse {0:format} và lấy format cho .toLocaleDateString(). Cho component khác (Number, Textbox, SearchEntry), nên dùng {FieldName:format} (theo tên field).

Ví dụ FormatCell (GridPolicy)

-- Cột FK hiển thị tên thay vì id
INSERT INTO GridPolicy (..., FieldName, ShortDesc, FormatCell, ...) VALUES
  (..., 'CustomerId',  N'Khách hàng', '{Customer.Name}',                 ...),
  (..., 'CreatedDate', N'Ngày tạo',   '{0:dd/MM/yyyy HH:mm}',            ...),
  (..., 'Total',       N'Tổng',       '{Total:N0}',                       ...),
  (..., 'Status',      N'Trạng thái', '{StatusName} ({StatusCode})',      ...);

FormatCell được Utils.FormatEntity áp dụng lên row entity (1 record của grid). Có thể navigate FK ({Customer.Name}).

GridPolicy còn field FormatRow — fallback khi FormatCell rỗng.

Logic render cell trong grid (Utils.GetCellTextInternal)

Khi grid render 1 cell, Core gọi GetCellTextInternal(component, cellData, row, refData, ...). Logic theo ComponentType của cột:

ComponentTypeCách áp FormatDataCú pháp placeholder
Datepickerstring.Format(FormatData ?? "{0:dd/MM/yyyy}", dateValue){0:format} (positional)
Dropdown / Select2Lookup ref entity từ refDataUtils.FormatEntity(FormatData, refEntity){Name} / {Code} (field name)
MultipleSearchEntrySplit CSV ids, mỗi id lookup ref entity → apply FormatData{Name} (field name)
Default (Textbox, Number, …)string.Format(FormatData, cellData) → if FormatEntity set, override → Utils.FormatEntity(FormatEntity, row)FormatData: {0:N2} (positional); FormatEntity: {FieldName} (field name)

Quy tắc:

  • Cell có FormatEntity set → luôn override FormatData (logic default branch).
  • Cell Dropdown/Select2/MultipleSearchEntry không dùng FormatEntity — chỉ FormatData áp lên ref entity.
  • Cell Datepicker dùng FormatData với {0:format}. Nếu rỗng → fallback {0:dd/MM/yyyy}.
  • Cell mặc định không set gì → text = cellData.ToString().

LoadMasterData — auto fetch reference data cho FK cells

Khi grid có cột FK (ComponentType = Dropdown/Select2/SearchEntry, RefName set), Core tự động gọi LoadMasterData(rows):

  1. Quét tất cả HeaderRefName set (vd RefName = "Customer").
  2. Lấy distinct ids từ rows hiện tại (vd CustomerId in (1,3,7)).
  3. POST Client.LoadById(...) để fetch các ref entity (Customer ids 1,3,7) — 1 request batch / RefName.
  4. Cache vào RefData[refName].
  5. SyncMasterData(rows, headers) — set property nested object trên row (vd row.Customer = customerEntity) để placeholder {Customer.Name} work.

Bạn không phải code gì — chỉ cần set RefName (hoặc Reference) trên GridPolicy của cột FK + FormatCell = "{Customer.Name}". Core lo phần fetch + bind.

Pattern bạn không nên làm:

# ❌ Sai — gọi API riêng để load Customer rồi tự ghép
FormatCell: "{0}"  # rồi code C# query Customer riêng cho mỗi row
# ✅ Đúng — set RefName, Core tự LoadMasterData + sync
RefName:    Customer
FormatCell: "{Customer.Name}"

Quy tắc ưu tiên (override)

  • Component có cả FormatData + FormatEntity set → cell render theo logic ComponentType:
    • Default branch: FormatEntity override FormatData.
    • Dropdown/Select2/MultipleSearchEntry: chỉ dùng FormatData.
    • Datepicker: chỉ dùng FormatData.
  • GridPolicy FormatCell được map sang FormatData → cùng quy tắc.
  • Không set gì → text thô.

Template (SearchEntry) khác FormatEntity?

FieldÁp dụng khi nào
TemplateTrong dropdown suggestion (lúc user đang gõ).
FormatEntitySau khi đã pick (hiển thị trong input).

Có thể set khác nhau:

ComType:       SearchEntry
FieldName:     CustomerId
Reference:     Customer
Template:      "{Code} - {Name}"          # dropdown
FormatEntity:  "{Name} ({Code}, {Phone})" # sau pick

Bẫy hay gặp

  • Dùng {0:N2} cho FormatEntity → sai. {0} chỉ hoạt động khi source là single value (FormatData), không hoạt động với entity. Dùng {FieldName:N2}.
  • Dùng tên field viết thường → sai. C# property names thường PascalCase (Code, Name), placeholder phải khớp.
  • Quên dấu : giữa field và format → {Total N2} sai, đúng phải là {Total:N2}.
  • Không escape { } trong template không phải placeholder → cần escape {{ }} (rare).

Events — wire method C# vào component

Field Component.Eventschuỗi JSON map event-type → tên method trên class TabEditor / PopupEditor. Khi user tương tác với widget, Core dispatch event tương ứng và tìm method trên class C# để gọi.

⚠ Quy tắc đặt tên event trong JSON

Loại eventCách viếtVí dụ
DOM eventlowercase (JS-style)click, change, input, keydown, focusin, dblclick, contextmenu
CustomEventTypePascalCase (như enum)BeforeCreated, AfterCreated, AfterChat, DateSearch, AfterDownload
DOMContentLoadedPascalCase đặc biệtDOMContentLoaded — viết y hệt vì là tên chuẩn DOM.

Sai chính tả case → method không bao giờ được gọi.

Cú pháp Events (DB là string, nội dung JSON)

{
  "click": "OnRowDeleted",
  "change": "OnQuantityChanged",
  "AfterCreated": "OnRowAdded"
}

3 cặp trên có nghĩa:

  • Khi component fire DOM event click → Core gọi form.OnRowDeleted(entity, ...).
  • Khi fire DOM event change → gọi form.OnQuantityChanged(entity, newValue, oldValue, ...).
  • Khi fire AfterCreated (CustomEventType, lifecycle event của Core) → gọi form.OnRowAdded(rowData).

Class C# của bạn chỉ cần khai báo method tên y hệt:

public class InvoiceDetailBL : PopupEditor
{
    public InvoiceDetailBL() : base(nameof(Invoice)) { Title = "Hóa đơn"; }

    public void OnRowDeleted(object entity, object btn)
    {
        Toast.Success("Đã xóa dòng");
    }

    public async Task OnQuantityChanged(object entity, object newVal, object oldVal, object parent)
    {
        // tự tính lại tổng, refresh field khác, ...
    }

    public void OnRowAdded(object rowData)
    {
        // setup default value cho row mới
    }
}

Method có thể void, async Task, async void đều được. Tham số đầu luôn là entity (record bound vào widget). Tham số tiếp theo tùy event (xem Tham chiếu component — phần Events của từng component).

Inline JavaScript thay vì method name

Value có thể là inline JS function (thay vì tên method):

{
  "click": "function(form, com) { console.log('clicked', form, com); }"
}

Core sẽ .Call(form, form, com) — function nhận form (instance TabEditor/PopupEditor) và com (component fire event). Dùng cho logic ngắn không đáng viết hẳn 1 method C#.

Event types

2 nhóm event:

NhómCách viếtKhi fireExamples
EventType (DOM)lowercaseUser tương tác trực tiếp (click, đổi value, focus…).click, change, input, keydown, dblclick, contextmenu, focusin
CustomEventTypePascalCaseLifecycle / business event riêng của Core.AfterCreated, BeforeDeleted, AfterPasted, RowFocusOut, AfterChat

Mỗi component fire 1 tập riêng — xem Tham chiếu component cho list event của từng ComType + method signature args nhận được.

Button đặc biệt

Row có ComType = "Button" thì Name quyết định behavior:

NameTự động làm
btnSaveGọi Save(Entity) của TabEditor/PopupEditor hiện tại.
btnCancelĐóng tab/popup (prompt nếu dirty).
btnPrintIn report bound.
btnPreviewPrint preview.
khácGọi method có tên trong Component.Events trên class C#.

Field HotKey (vd "Ctrl+S") auto wire phím tắt.

Cách Core load 1 feature

Khi bạn mở 1 màn hình:

  1. Core lookup Feature theo Name.
  2. Load tất cả ComponentGroup thuộc feature, build cây qua ParentId.
  3. Với mỗi section, load Component con.
  4. Với mỗi component, instantiate class EditableComponent tương ứng ComType (qua reflection).
  5. Bind tới Entity qua FieldName.
  6. Render.

Bạn không phải biết chi tiết — chỉ cần biết insert đủ row đúng thì màn hình hiện ra.

Sửa cấu hình ở đâu?

Không ở source code. Trong app đang chạy:

  1. Đăng nhập với account admin.
  2. Mở menu Quản lý Feature.
  3. Tạo / sửa row Feature.
  4. Click vào feature → mở Detail Feature → tab Section thêm ComponentGroup, tab Component thêm Component.
  5. Save → F5 lại trang test → cấu hình mới có hiệu lực ngay.

Không cần rebuild C# / không cần restart backend.

Đánh đổi của low-code

Lợi:

  • Thêm field không cần biên dịch lại.
  • Reorder field bằng UI.
  • Variation per-tenant (cùng code, row khác nhau).
  • Validation tập trung trong field config.
  • Permission per-field qua FeaturePolicy.

Phải nhớ:

  • “Field này render ở đâu?” trả lời bằng query DB, không grep code.
  • ComType mới (custom widget) vẫn cần build C#.
  • Sửa schema entity (model) vẫn cần deploy.

Bẫy hay gặp khi config

Triệu chứngKiểm tra
Tab mở trắngSection thiếu hoặc all Active=false.
Field không hiệnActive=true? Visibility expression có ra true?
Console: “property X not found”FieldName không có trên entity model.
Grid không có cộtThiếu GridPolicy cho grid đó.
Button không làm gìTên method (case-sensitive) có khớp Component.Events?

Đi tiếp

Core Docs · Astro · Core.API/wwwRoot/docs