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.LinesquaSavemaster để bắt issue cross-line (vd duplicate product). - Giữ FK consistent. User add 1 line → set
InvoiceIdvề 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.