Core Docs

Master / detail

1 entity cha + nhiều collection con — pattern form line-of-business chuẩn.

Phần lớn màn hình edit trong Core có hình:

  • 1 header chứa các field master (Code, Name, ngày, status).
  • 1 hoặc nhiều grid con (Lines, Attachments, Notes).
  • Save commit cả gói cùng lúc.

Đây là setup chuẩn.

Hình dạng dữ liệu

public class Invoice
{
    public int      Id { get; set; }
    public string   Code { get; set; }
    public DateTime Date { get; set; }
    public int      CustomerId { get; set; }
    public List<InvoiceLine> Lines { get; set; }       // children
    public List<Attachment>  Attachments { get; set; }
}

public class InvoiceLine
{
    public int     Id { get; set; }
    public int     InvoiceId { get; set; }
    public int     ProductId { get; set; }
    public decimal Qty { get; set; }
    public decimal Price { get; set; }
}

Load

Dùng $expand để API trả children inline:

protected override async Task<object> LoadEntity()
{
    var invoice = await new Client(nameof(Invoice))
        .GetAsync<Invoice>("?$expand=Lines,Attachments&$filter=Id eq " + EntityId);
    Entity = invoice;
    return await base.LoadEntity();
}

LoadEntity mặc định gọi GetAsync<T>(EntityId) không expand. Override khi cần related data.

Layout (cấu hình DB)

Feature: Invoice
└── ComponentGroup: Header
    ├── Component: Code         (Textbox)
    ├── Component: Date         (Datepicker)
    └── Component: CustomerId   (SearchEntry, Reference: Customer)
└── ComponentGroup: LinesPanel
    └── Component: LinesGrid    (GridView, FieldName: Lines, Reference: InvoiceLine, Editable: true)
└── ComponentGroup: Attachments
    └── Component: AttachGrid   (GridView, FieldName: Attachments, Reference: Attachment)

FieldName của row Component grid trỏ vào navigation collection. Grid dùng list đó làm row source — add/edit/remove mutate entity.Lines.

Inline-edit grid

Cờ trên row Component:

ComType: GridView
Editable: true

Grid render thêm 1 empty row dưới cùng. Khi user gõ vào cell, framework promote row đó thành record thật, set InvoiceId từ entity cha, push vào entity.Lines. 1 empty row mới hiện ra.

ListViewItem.EmptyRow = true cho placeholder — input check cờ này để bỏ qua validation required.

Save — 2 chiến lược

A. 1 round-trip, nested

public override async Task<bool> Save(object entity)
{
    var invoice = (Invoice)entity;
    return await base.Save(invoice);   // base POST /api/Invoice với allowNested
}

Yêu cầu server chấp nhận nested write — controller phải deserialize và persist children. Dễ nhất khi bạn control cả 2 phía.

B. 2 pass (parent rồi children)

public override async Task<bool> Save(object entity)
{
    var invoice = (Invoice)entity;
    var lines   = invoice.Lines ?? new List<InvoiceLine>();

    // 1) save parent không kèm children
    invoice.Lines = null;
    var ok = await base.Save(invoice);
    if (!ok) return false;

    // 2) bulk save children với FK đã populate
    foreach (var l in lines)  l.InvoiceId = invoice.Id;
    await new Client(nameof(InvoiceLine)).BulkUpdateAsync(lines);

    invoice.Lines = lines;
    return true;
}

Dài nhưng đoán được: server thấy 1 POST per resource type. Dùng khi nested update không hỗ trợ, hoặc cần validation/permission khác cho mỗi loại child.

Refresh grid cha sau save

Editor mở từ list — list không tự biết phải refresh. 2 lựa chọn:

// Cách 1: AfterSaved trên editor
AfterSaved = ok =>
{
    if (!ok) return;
    var list = TabEditor.Tabs.OfType<InvoiceListBL>().FirstOrDefault();
    list?.FindComponentByName<GridView>("Grid")?.ReloadDataAsync();
};

// Cách 2: ShouldUpdateParentForm = true để framework tự lo
ShouldUpdateParentForm = true;

ShouldUpdateParentForm trigger refresh chung — đa số trường hợp đủ.

Cascade delete

Grid con fire RowData.OnRemove. Cần xoá trên server cùng:

linesGrid.RowData.OnRemove += async row =>
{
    var line = (InvoiceLine)row;
    if (line.Id > 0)
    {
        await new Client(nameof(InvoiceLine)).HardDeleteAsync(line.Id);
    }
};

Hoặc đơn giản hơn: track id đã xoá trong list DeletedIds, xử lý cùng save.

Edge case: child có editor popup riêng

Một số grid con mở popup riêng thay vì inline-edit:

linesGrid.DblClick = row =>
{
    var line = (InvoiceLine)row;
    var dialog = new InvoiceLineEditorBL { Entity = line };
    dialog.AfterSaved = ok =>
    {
        if (ok) linesGrid.UpdateRow(line);
    };
    AddChild(dialog);
};

Popup edit row tại chỗ; grid re-render đúng row đó.

Tip

  • Đừng nest popup quá sâu. Popup-từ-popup-từ-popup là bẫy UX. Ưu tiên inline-edit grid hoặc panel side-by-side.
  • Validate ở parent. Dù children có validation riêng, vẫn pass entity.Lines qua Save master để bắt issue cross-line (vd duplicate product).
  • Giữ FK consistent. User add 1 line → set InvoiceId về id cha ngay (kể cả Id = 0), update lại sau khi parent save xong. Bỏ bước này = nguồn #1 của orphan children.

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