AstrolBoard<TBox, TRelation> pinta un Board<TBox, TRelation>: una lista de cajas (Box<TBox>, con su contenido de tipo TBox) y de relaciones entre ellas (Relation<TRelation>). Usted decide cómo se ve cada caja con BoxRender; el tablero modifica la posición y el tamaño en el mismo objeto y avisa con OnChangeBoard para que lo guarde.
Move a box.
@using Astrol.Component.Board@using Astrol.Entities.Basic.UI.Board @* Drag the boxes, resize them from the corners, Ctrl+click to select several, arrows to move them, Ctrl+Z / Ctrl+Y, Ctrl+F to search and Ctrl+wheel to zoom. *@<div style="height:380px"> <AstrolBoard TBox="WorkItem" TRelation="object" Board="board" BackgroundType="BackgroundType.Points" MinWidth="80" MinHeight="60" OnChangeBoard="OnChange"> <BoxRender> <div style="height:100%; padding:8px; border-radius:8px; border:1px solid var(--color-gray-a); background:var(--layout-menu-bg); overflow:hidden"> <b>@context.BOX_NAME</b> <div style="font-size:.8em; opacity:.8">@context.Content.Owner</div> </div> </BoxRender> </AstrolBoard></div><p aria-live="polite">@message</p> @code { private string message = "Move a box."; private readonly Board<WorkItem, object> board = Create(); private void OnChange(Board<WorkItem, object> changed) { var first = changed.Boxes[0]; message = $"{first.BOX_NAME}: X {first.X:0}, Y {first.Y:0}, {first.Width:0} x {first.Height:0}"; } private static Board<WorkItem, object> Create() { var b = new Board<WorkItem, object>(); b.Boxes.Add(new Box<WorkItem> { X = 30, Y = 40, Width = 160, Height = 90, BOX_NAME = "Design", Content = new WorkItem { Owner = "Ana" } }); b.Boxes.Add(new Box<WorkItem> { X = 290, Y = 40, Width = 160, Height = 90, BOX_NAME = "Build", Content = new WorkItem { Owner = "Luis" } }); b.Boxes.Add(new Box<WorkItem> { X = 290, Y = 220, Width = 160, Height = 90, BOX_NAME = "Test", Content = new WorkItem { Owner = "Eva" } }); b.Relations.Add(new Relation<object> { BoxIdA = b.Boxes[0].Id, BoxIdB = b.Boxes[1].Id }); b.Relations.Add(new Relation<object> { BoxIdA = b.Boxes[1].Id, BoxIdB = b.Boxes[2].Id }); return b; } public class WorkItem { public string Owner { get; set; } = string.Empty; }}<AstrolBoard TBox="Task" TRelation="object" Board="board" ShowBoardContextMenu="true" ShowBoxContextMenu="true" OnOpenBoardContextMenu="p => { position = p; }" OnOpenBoxContextMenu="e => { (boxId, position) = e; }"> <BoardContextMenu> <MenuOpion NeedAuthorize="false" Icon="astrol-plus" Text="New box here" OnClick="AddBox" /> </BoardContextMenu> <BoxContextMenu> <MenuOpion NeedAuthorize="false" Icon="astrol-clone" Text="Duplicate" OnClick="Duplicate" HasSeparator="true" /> <MenuOpion NeedAuthorize="false" Icon="astrol-trash" IconColor="var(--aui-danger)" Text="Delete" OnClick="Delete" /> </BoxContextMenu> ...</AstrolBoard>private Position position = new();private string? boxId; // The position is in board pixels: divide by Scale to get model unitsprivate void AddBox() => board.Boxes.Add(new Box<Task> { X = position.X / board.Scale, Y = position.Y / board.Scale, BOX_NAME = "New" });private void Delete() => board.Boxes.RemoveAll(b => b.Id == boxId);<AstrolBoard TBox="Task" TRelation="object" Board="board" RelationAnchor="RelationAnchor.Border" LineStyle="RelationLineStyle.Curved" EditAnchors="true"> ... </AstrolBoard>// From the middle of the right side of A to the top quarter of the left side of Bboard.Relations.Add(new Relation<object>{ BoxIdA = a.Id, BoxIdB = b.Id, AnchorA = BoxAnchor.Right, // (1, 0.5) AnchorB = new BoxAnchor(0, 0.25F), // x and y from 0 to 1, relative to the box});<AstrolBoard TBox="Task" TRelation="object" Board="board" @ref="boardRef" RelationAnchor="RelationAnchor.Border" RelationArrow="RelationArrow.End" AllowConnect="true" OnConnect="Connect" BackgroundDrag="BoardBackgroundDrag.Select" SnapToGrid="20" SnapToBoxes="true" OnBoxesAdded="Added" OnBoxesRemoved="Removed"> <RelationRender>@context.Label</RelationRender> ...</AstrolBoard>// The relation is not on the board yet: complete it or cancel itprivate void Connect(BoardConnectEventArgs<object> e){ e.Relation.Label = "next"; e.Cancel = board.Relations.Any(r => r.BoxIdA == e.Relation.BoxIdA && r.BoxIdB == e.Relation.BoxIdB);} board.Relations.Add(new Relation<object> { BoxIdA = a.Id, BoxIdB = b.Id, Label = "Yes", Arrow = RelationArrow.Both });<button @onclick="Arrange">Arrange</button><AstrolBoard TBox="Task" TRelation="object" Board="board" @ref="boardRef"> ... </AstrolBoard>private AstrolBoard<Task, object>? boardRef; private Task Arrange() => boardRef!.ArrangeAsync(new BoardLayoutOptions{ Direction = BoardLayoutDirection.LeftToRight, LayerSpacing = 60,}); // Without the component (e.g. before showing the board):BoardLayout.Arrange(board);Con ShowGuides="true", al arrastrar o redimensionar la caja seleccionada aparecen líneas guía discontinuas desde sus esquinas (hacia afuera) y desde el centro de sus lados (más tenues), hasta el borde del tablero, para alinearla con otras cajas. Por defecto están desactivadas. Cada caja puede forzarlas con Box.ShowGuides (true / false; null sigue al tablero). El aspecto se cambia con variables CSS:
<AstrolBoard TBox="Task" TRelation="object" Board="board" ShowGuides="true"> ... </AstrolBoard>// Per box: null = follow the board, true / false = force it for this boxboard.Boxes.Add(new Box<Task> { BOX_NAME = "Title", ShowGuides = false });/* Optional: line style, separation and opacity of the side lines */:root { --border-line: 1px dashed var(--color-red-transparent); --pbse-offset: 2px; --pbse-side-opacity: 0.45;}Con @ref (private AstrolBoard<Task, object>? boardRef;):