Core Docs

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. Warehouse có controller /api/Warehouse và model với ít nhất Id, 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>BL cho list, <Entity>DetailBL cho editor. Xem TMS.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:

FieldGiá trị
NameWarehouse
LabelKho hàng
EntityNameWarehouse
Iconicons/warehouse.png
Activetrue

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:

FieldGiá trị
LabelKho hàng
ActionWarehouseListBL (tên class C# vừa tạo)
Iconicons/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:

FieldGiá trị
NameBody
ElementTypediv
ClassNamefeature-body
Order10
Activetrue

Trong section Body, thêm 1 row Component cho grid:

FieldGiá trị
NameGrid
ComTypeGridView
FieldName(rỗng — auto load)
ReferenceWarehouse
DataSourceFilter?$filter=Active eq true (chuỗi OData querystring đủ ?$filter=)
Order10

Refresh, mở Kho hàng — sẽ thấy grid rỗng (chưa có cột).

DataSourceFilter bắ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 GridView thự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: 
  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")) (xem DriverContainerDetailBL.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: 
  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 ComType chỉ đọ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

  1. Mở Kho hàng (tab list).
  2. Double-click 1 row → editor popup mở ra.
  3. Sửa Tên, Ctrl+S → toast “Đã lưu”.
  4. 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 trong Component.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 Feature tồn tại (Active = true, EntityName khớp).
  • Ít nhất 1 ComponentGroup (section) thuộc feature.
  • Component đã cấu hình FieldName, ComType, Order.
  • GridPolicy cho grid cột.
  • btnSave (và btnCancel/btnPrint…) đã thêm khi cần.
  • Validation rule set vào Component.Validation (DB) + override Save gọi await 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ở FeatureDetailBL của feature để check.
  • Grid render nhưng không có cột. Thiếu GridPolicy scope vào ComponentId của grid.
  • Console: “Cannot read property ‘X’ of undefined”. Gần như chắc chắn: input bind FieldName khô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ùng BulkUpdateAsync riêng cho children, hoặc set allowNested: true — xem Lưu entity trước.
  • Tab open 2 lần thấy 2 instance. Bạn dùng id khác nhau — OpenTab key theo id, đổi id mỗi lần gọi sẽ tạo tab mới.

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