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(quathis.<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ần | Dùng helper |
|---|---|
| Mở list → tab full | this.OpenTab(id, ...) |
| Mở edit 1 record → popup, ID ổn định | this.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 trongBeforeSavedthủ công — set vàoComponent.Validationcủ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 button | Hot key thường gặp |
|---|---|
btnSave | Ctrl+S |
btnCancel | Esc |
btnPrint | Ctrl+P |
btnSearch | F5 |
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 TabEditorkhô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. Clientxử lý auth, retry 502, tenant routing, error toast — bypass = mất hết.