Core Docs

Helpers thường dùng

Các method helper bạn sẽ gọi mỗi ngày khi viết màn hình. Chỉ liệt kê những cái user app thực sự dùng — không phải toàn bộ extension internal.

Các helper dưới đây gọi từ trong class TabEditor / PopupEditor (qua this.<MethodName>(...)) hoặc gọi static (Toast.Success(...)).

🔍 Tìm widget trong màn hình

// Tìm 1 component theo Name (Name là field trong row Component cấu hình DB)
var grid = this.FindComponentByName<GridView>("Grid");
var input = this.FindComponentByName<Textbox>("Code");

→ Dùng để truy cập widget sau khi DOM render xong (trong DOMContentLoaded).

🚪 Mở tab / popup

// Mở tab full màn hình
this.OpenTab("CustomerList", "Customer", () => new CustomerListBL());

// Mở popup modal (cách ngắn — id auto từ hash)
await this.Popup(nameof(Customer), () => new CustomerDetailBL { Entity = c });

// Mở tab/popup với id cố định (mở 2 lần thì focus tab cũ)
await this.OpenTab("Customer_42", "Customer",
    () => new CustomerDetailBL { Entity = c },
    popup: true);
CầnDùng helper
Mở list → tab fullthis.OpenTab(id, ...)
Mở edit 1 record → popup, ID ổn địnhthis.OpenTab(id, ..., popup: true)
Mở 1 dialog tạm (1 instance / màn hình)this.Popup(...)

🎯 Ẩn / hiện / disable widget

// Ẩn nhiều field theo Name
this.SetShow(false, "FieldA", "FieldB");

// Hiện lại
this.SetShow(true, "FieldA", "FieldB");

// Disable nhiều field
this.SetDisabled(true, "FieldA", "FieldB");

// Ẩn / disable 1 widget cụ thể
component.Show = false;
component.Disabled = true;

Pattern thực: DriverContainerDetailBL.cs ẩn label trên DOMContentLoaded:

this.SetShow(false, "LabelCut", "LabelNeo");
this.SetDisabled(CoorDEntity.EmptyContFromId != null, nameof(CoorDEntity.EmptyContFromId));

💬 Toast notification

Toast.Success("Đã lưu");
Toast.Warning("Vui lòng chọn ít nhất 1 dòng");
Toast.Small("Đã copy");        // toast nhỏ ít gây chú ý

3 loại trên đủ cho 95% trường hợp. Không có Toast.Error chính thức — dùng Toast.Warning cho lỗi.

❓ ConfirmDialog (yes/no)

var confirm = new ConfirmDialog
{
    Title      = "Xác nhận",
    Content    = "Bạn có chắc muốn xóa?",
    NeedAnswer = false,         // true → có thêm textarea
    TabEditor  = TabEditor,     // anchor vào màn hình hiện tại
};
confirm.YesConfirmed += async () =>
{
    // user bấm Yes
};
confirm.Render();   // ⚠ phải gọi Render() thủ công

Khi NeedAnswer = true, đọc input qua confirm.Textbox.Text (single line) hoặc confirm.Textarea.Value (multi-line, đặt thêm ComType = nameof(Textarea)).

📋 Lấy dữ liệu từ grid

var grid = this.FindComponentByName<GridView>("Grid");

// Lấy các dòng đang chọn (checkbox / row click với Ctrl)
var selected = grid.GetSelectedRows();
if (selected.Nothing())   // extension: !list.Any()
{
    Toast.Warning("Vui lòng chọn dòng");
    return;
}

// Cast sang entity
var customers = selected.Cast<Customer>().ToList();

Nothing() là extension Core thay cho !list.Any() — dùng cho readability.

🌐 Gọi API

using Core.Clients;

// GET 1 record
var customer = await new Client(nameof(Customer)).GetAsync<Customer>(42);

// GET list (OData)
var rows = await new Client(nameof(Customer))
    .GetRawList<Customer>("?$filter=Active eq true&$top=100");

// GET first or default
var version = await new Client(nameof(Entity))
    .FirstOrDefaultAsync<Entity>("?$top=1&$filter=Name eq 'Version'");

// POST
var saved = await new Client(nameof(Customer)).PostAsync<Customer>(customer);

// Bulk update (mix insert/update)
await new Client(nameof(InvoiceLine)).BulkUpdateAsync(lines);

// Hard delete
await new Client(nameof(Customer)).HardDeleteAsync(id);
await new Client(nameof(Customer)).HardDeleteAsync(new List<int> { 1, 2, 3 });

→ Chi tiết tại Client (REST + OData).

🎨 Format / parse

using Core.Extensions;

// Format DateTime nullable
var s = date.CustomFormat("dd/MM/yyyy");

// Set property theo path có dấu chấm
entity.SetComplexPropValue("Customer.Address.City", "Hà Nội");

// Cast property an toàn (Bridge)
var feature = obj.CastProp<Feature>();

⏳ Spinner (loading toàn cục)

Spinner.AppendTo(Document.Body);    // hiện spinner
try
{
    var data = await LongRunningCall();
}
finally
{
    Spinner.Hide();                 // luôn hide trong finally
}

🔁 Refresh sau khi save

public class CustomerDetailBL : PopupEditor
{
    public CustomerDetailBL() : base(nameof(Customer))
    {
        ShouldUpdateParentForm = true;   // cách 1: framework auto refresh tab cha

        // hoặc cách 2: handle thủ công
        AfterSaved = ok =>
        {
            if (!ok) return;
            Dispose();   // đóng popup
            var list = TabEditor.Tabs.OfType<CustomerListBL>().FirstOrDefault();
            list?.FindComponentByName<GridView>("Grid")?.ReloadDataAsync();
        };
    }
}

✅ Validation trước save

Cách 1 — Cấu hình rule trong DB (Component.Validation):

Trong row Component của field cần validate, set field Validation với rule (regex / required / range / custom expression). Core đọc và check tự động khi user blur / submit.

# Trong row Component
ComType:    Textbox
FieldName:  Code
Validation: |
  {
    "required": true,
    "minLength": 3,
    "maxLength": 20,
    "pattern": "^[A-Z0-9-]+$",
    "message": "Mã chỉ chứa chữ in hoa, số và dấu '-', dài 3-20 ký tự"
  }

(Format JSON cụ thể tham khảo các row có sẵn trong DB; mỗi rule + message hiển thị khi sai.)

Cách 2 — Trước khi submit, gọi await this.IsFormValid():

IsFormValid() duyệt mọi widget visible, gọi ValidateAsync() → check Validation config + Required + minLength + … → return true nếu hợp lệ. Nếu sai, tự focus field đầu tiên + show toast lỗi.

public override async Task<bool> Save(object entity)
{
    if (!await this.IsFormValid()) return false;   // tự show message + focus field sai
    return await base.Save(entity);
}

IsFormValid() đã được Core gọi tự động trong Save mặc định — bạn chỉ cần override khi muốn check thêm điều kiện business riêng:

public override async Task<bool> Save(object entity)
{
    // 1) Validation cấu hình DB (Required, regex, ...) — tự động gọi
    if (!await this.IsFormValid()) return false;

    // 2) Custom business check
    var c = (Customer)entity;
    if (!await ValidationExtensions.IsUnique(c, nameof(Customer.Code)))
    {
        Toast.Warning("Mã đã tồn tại");
        return false;
    }

    return await base.Save(c);
}

Đừng check Required / regex / minLength trong BeforeSaved thủ công — set vào Component.Validation của row DB rồi để Core lo. Code C# chỉ cho business rule không cấu hình được (vd check unique từ server, validate cross-field phức tạp).

📌 Context menu cho grid (chuột phải row)

private void WireContextMenu()
{
    var grid = this.FindComponentByName<GridView>("Grid");
    if (grid is null) return;

    grid.BodyContextMenuShow += () =>
    {
        ContextMenu.Instance.MenuItems = new List<ContextMenuItem>
        {
            new ContextMenuItem { Icon = "fas fa-pen",   Text = "Sửa",  Click = OnEdit },
            new ContextMenuItem { Icon = "fas fa-times", Text = "Xóa",  Click = OnDelete },
        };
    };
}

private void OnEdit(object arg) { /* ... */ }
private void OnDelete(object arg) { /* ... */ }

→ Pattern thực CoordinationContainerBL.cs.

🎹 Phím tắt button

Trong cấu hình DB, set Component.HotKey cho row Button:

Tên buttonHot key thường gặp
btnSaveCtrl+S
btnCancelEsc
btnPrintCtrl+P
btnSearchF5

Core auto-wire — không cần code thêm.

❌ Đừng làm thế này

// ❌ Tự new TabEditor
new TabEditor("Customer").Render();

// ❌ Truy cập DOM thẳng bằng Document.GetElementById
var el = Document.GetElementById("input-1");

// ❌ Dùng XHR/fetch riêng
var xhr = new XMLHttpRequest();
xhr.Open("GET", "/api/Customer");

Lý do:

  • Tự new TabEditor không vào registry, không có entity, không có config — sẽ crash.
  • DOM ID động, không stable. Dùng FindComponentByName<T>("Name") an toàn hơn.
  • Client xử lý auth, retry 502, tenant routing, error toast — bypass = mất hết.

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