Cách tạo 1 màn hình mới
Đi từ "có entity" tới "có 1 tab chạy được trong app" theo pattern thực tế của TMS.UI.
Trang này dẫn từng bước build 1 feature Warehouse (Kho hàng) giả định, theo đúng pattern các màn hình thực trong TMS.UI/Business/. Chọn các bước cần — không phải feature nào cũng cần đủ.
Mảng kiến thức cần biết trước
- 3 bảng cấu hình UI:
Feature,ComponentGroup,Component— xem Feature data model. - Class cha hay dùng:
EditForm/TabEditor/PopupEditor. - Cấu hình cột grid:
GridPolicy.
Tiền đề
- Entity tồn tại bên server.
Warehousecó controller/api/Warehousevà model với ít nhấtId,Code,Name,Active. - Bạn truy cập được màn hình Quản lý Feature (
FeatureBL) trong app đang chạy — dùng để insert row config. (Hoặc seed bằng SQL nếu chưa có.)
1 · Thêm class C# ở TMS.UI
Tạo file TMS.UI/Business/Warehouse/WarehouseListBL.cs:
using Core.Components.Forms;
namespace TMS.UI.Business.Warehouse
{
public class WarehouseListBL : TabEditor
{
public WarehouseListBL() : base(nameof(Warehouse))
{
Title = "Kho hàng";
Icon = "icons/warehouse.png";
}
}
}
Đủ để có 1 tab rỗng. Build TMS.UI, refresh — WarehouseListBL giờ là ComType runtime instantiate được.
Pattern naming:
<Entity>BLcho list,<Entity>DetailBLcho editor. XemTMS.UI/Business/Freight/CoordinationContainerBL.cs(list) vàDriverContainerDetailBL.cs(detail) làm tham chiếu.
2 · Đăng ký Feature trong DB
Mở Feature management (FeatureBL) trong app rồi thêm 1 row:
| Field | Giá trị |
|---|---|
Name | Warehouse |
Label | Kho hàng |
EntityName | Warehouse |
Icon | icons/warehouse.png |
Active | true |
Save. Feature đã tồn tại nhưng chưa có section/component.
3 · Thêm vào Menu
Mở màn hình quản lý menu (build trên Core/Fw/Menu.cs), thêm 1 menu item mở feature:
| Field | Giá trị |
|---|---|
Label | Kho hàng |
Action | WarehouseListBL (tên class C# vừa tạo) |
Icon | icons/warehouse.png |
ParentId | (group menu phù hợp) |
Portal render menu, click sẽ gọi OpenTab("Warehouse", () => new WarehouseListBL()).
4 · Thêm Section + Grid
Mở FeatureDetailBL cho Warehouse. Thêm 1 row ComponentGroup:
| Field | Giá trị |
|---|---|
Name | Body |
ElementType | div |
ClassName | feature-body |
Order | 10 |
Active | true |
Trong section Body, thêm 1 row Component cho grid:
| Field | Giá trị |
|---|---|
Name | Grid |
ComType | GridView |
FieldName | (rỗng — auto load) |
Reference | Warehouse |
DataSourceFilter | ?$filter=Active eq true (chuỗi OData querystring đủ ?$filter=) |
Order | 10 |
Refresh, mở Kho hàng — sẽ thấy grid rỗng (chưa có cột).
DataSourceFilterbắt buộc đủ?$filter=...ở đầu. Có thể thêm$expand/$orderby/$top(vd?$expand=Country&$filter=Active eq true&$top=200) hoặc placeholder{CustomerId}(curly braces, substitute từ entity). Chi tiết: Cấu hình UI → DataSourceFilter.
🔍 Field DB nào
GridViewthực sự đọc — xem Trang GridView.
5 · Cấu hình cột (GridPolicy)
Với mỗi cột muốn có, thêm 1 row GridPolicy scope theo (FeatureId = Warehouse, ComponentId = Grid):
- FieldName: Code
ShortDesc: Mã
Width: 120
Frozen: true
Sortable: true
Order: 10
- FieldName: Name
ShortDesc: Tên kho
Width: 240
Sortable: true
Order: 20
- FieldName: Active
ShortDesc: Hoạt động
Width: 80
ComType: Checkbox
Order: 30
Refresh — cột xuất hiện, grid sort được.
6 · Build editor (popup)
Tạo WarehouseDetailBL.cs:
using Core.Components.Forms;
namespace TMS.UI.Business.Warehouse
{
public class WarehouseDetailBL : PopupEditor
{
public WarehouseDetailBL() : base(nameof(Warehouse))
{
Title = "Kho hàng";
Icon = "icons/warehouse.png";
ShouldLoadEntity = true; // GET /api/Warehouse/{id}
}
}
}
Wire double-click row → mở editor. Cách sạch nhất: hook DblClick của grid trong list (DOMContentLoaded chạy sau khi DOM sẵn sàng):
public class WarehouseListBL : TabEditor
{
public WarehouseListBL() : base(nameof(Warehouse))
{
Title = "Kho hàng";
Icon = "icons/warehouse.png";
DOMContentLoaded += WireGridDblClick;
}
private void WireGridDblClick()
{
var grid = this.FindComponentByName<GridView>("Grid");
if (grid is null) return;
grid.DblClick = row =>
{
var w = (Warehouse)row;
this.OpenTab("Warehouse_" + w.Id,
() => new WarehouseDetailBL { Entity = w });
};
}
}
Pattern dùng
Activator.CreateInstance(Type.GetType("TMS.UI.Business.Warehouse.WarehouseDetailBL"))(xemDriverContainerDetailBL.Neo()của TMS.UI) hữu ích khi tránh hard reference giữa các module.
7 · Cấu hình các field của editor
Trong FeatureDetailBL cho Warehouse, thêm ComponentGroup thứ 2 tên EditPanel:
ComponentGroup:
Name: EditPanel
ElementType: div
ClassName: edit-panel
Order: 20
Bên trong, thêm row Component:
- Name: Code
ComType: Textbox
FieldName: Code
Label: Mã
ShowLabel: true
UpperCase: true
Order: 10
- Name: Name
ComType: Textbox
FieldName: Name
Label: Tên
ShowLabel: true
Order: 20
- Name: Active
ComType: Checkbox
FieldName: Active
Label: Đang hoạt động
Order: 30
- Name: btnSave
ComType: Button
Label: Lưu
HotKey: Ctrl+S
Order: 99
Button btnSave tự wire vào Save(Entity) vì khớp tên EditForm.BtnSave.
Mỗi
ComTypechỉ đọc 1 tập field nhất định — tra Tham chiếu component trước khi cấu hình. Đặt field component không đọc cũng vô tác dụng.
8 · Test luồng
- Mở Kho hàng (tab list).
- Double-click 1 row → editor popup mở ra.
- Sửa Tên, Ctrl+S → toast “Đã lưu”.
- Grid cha vẫn show data cũ → wire
AfterSavedđể refresh:
public WarehouseDetailBL() : base(nameof(Warehouse))
{
Title = "Kho hàng";
ShouldLoadEntity = true;
AfterSaved = ok =>
{
if (!ok) return;
Dispose();
var list = TabEditor.Tabs.OfType<WarehouseListBL>().FirstOrDefault();
list?.FindComponentByName<GridView>("Grid")?.ReloadDataAsync();
};
}
9 · Validation
Cấu hình rule vào field Component.Validation của từng row DB (Required, regex, minLength, range…). Trước submit, Core gọi IsFormValid() để check.
Code C# chỉ cần override Save cho business rule (unique check, cross-field):
public override async Task<bool> Save(object entity)
{
// Validate cấu hình DB (Required + regex + ...) — chuẩn nhất
if (!await this.IsFormValid()) return false;
// Business check riêng
var w = (Warehouse)entity;
if (!await ValidationExtensions.IsUnique(w, nameof(Warehouse.Code)))
{
Toast.Warning("Mã kho đã tồn tại");
return false;
}
return await base.Save(w);
}
Đừng tự check
IsNullOrWhiteSpace(), regex pattern, length trong code — set rule trongComponent.Validation(DB) rồi để Core check tự động.
10 · Pattern thực: context menu cho row
Pattern hay từ CoordinationContainerBL.cs của TMS.UI — gắn context menu (chuột phải) vào grid:
private void WireContext()
{
var grid = this.FindComponentByName<GridView>("Grid");
if (grid is null) return;
grid.BodyContextMenuShow += () =>
{
ContextMenu.Instance.MenuItems = new List<ContextMenuItem>
{
new() { Icon = "fas fa-pen", Text = "Sửa", Click = OnEdit },
new() { Icon = "fas fa-times", Text = "Xoá", Click = OnDelete },
};
};
}
11 · Pattern thực: kiểm tra lựa chọn
public void OnDelete(object arg)
{
var grid = this.FindComponentByName<GridView>("Grid");
var selected = grid.GetSelectedRows();
if (selected.Nothing())
{
Toast.Warning("Vui lòng chọn dòng cần xoá");
return;
}
var confirm = new ConfirmDialog
{
Title = "Xác nhận",
Content = "Xoá những kho đã chọn?",
TabEditor = TabEditor,
};
confirm.YesConfirmed += async () =>
{
var ids = selected.Select(x => ((Warehouse)x).Id).ToList();
await new Client(nameof(Warehouse)).HardDeleteAsync(ids);
await grid.ReloadDataAsync();
Toast.Success("Đã xoá");
};
confirm.Render();
}
(Nothing() = extension trong Core.Extensions thay cho !list.Any().)
12 · Pattern thực: thêm grid con (master/detail)
Xem Master / detail — thêm ComponentGroup con + GridView FieldName: Lines Reference: WarehouseZone để có grid con bind vào navigation collection của entity.
Checklist trước khi commit
- Class C# đã compile vào bundle FE.
- Menu item trỏ vào class.
- Row
Featuretồn tại (Active = true,EntityNamekhớp). - Ít nhất 1
ComponentGroup(section) thuộc feature. -
Componentđã cấu hìnhFieldName,ComType,Order. - Có
GridPolicycho grid cột. -
btnSave(và btnCancel/btnPrint…) đã thêm khi cần. - Validation rule set vào
Component.Validation(DB) + overrideSavegọiawait this.IsFormValid()cho business check. - Grid cha refresh sau save.
Trục trặc hay gặp
- Tab mở trắng. Section thiếu hoặc tất cả component
Active = false. MởFeatureDetailBLcủa feature để check. - Grid render nhưng không có cột. Thiếu
GridPolicyscope vàoComponentIdcủa grid. - Console: “Cannot read property ‘X’ of undefined”. Gần như chắc chắn: input bind
FieldNamekhông tồn tại trên entity. Sửa chính tả hoặc thêm property. - Save success nhưng dữ liệu không đổi. Nhiều khả năng
allowNested = false(mặc định) và bạn cố save children inline. DùngBulkUpdateAsyncriêng cho children, hoặc setallowNested: true— xem Lưu entity trước. - Tab open 2 lần thấy 2 instance. Bạn dùng id khác nhau —
OpenTabkey theo id, đổi id mỗi lần gọi sẽ tạo tab mới.