Summary#
Marks form a stack on the unit of work. UnitOfWork.Mark(label) records everything staged so far and answers
a UnitOfWorkMark; UnitOfWork.Undo() rolls back to the newest mark and pops it — what Ctrl+Z does;
UnitOfWork.Redo() re-applies what the last Undo took back, until a new edit clears it; UnitOfWork.RollbackTo(m)
jumps back past every later mark. Creates, updates, deletes and collection membership all roll back. Nothing is sent
anywhere until a commit, and a commit clears the marks.
Signature#
UnitOfWorkMark UnitOfWork.Mark() // push a mark; the label is ""
UnitOfWorkMark UnitOfWork.Mark(string label) // …with a label an Undo button can show ("Move Approve?")
void UnitOfWork.Undo() // back to the newest mark, which is popped — nothing to undo: nothing happens
void UnitOfWork.Redo() // re-apply what the last Undo / RollbackTo took back
void UnitOfWork.RollbackTo(UnitOfWorkMark m) // back to m, popping it and every mark after it
bool UnitOfWork.CanUndo() · UnitOfWork.CanRedo()
string UnitOfWork.UndoLabel() · UnitOfWork.RedoLabel() // "" when there is none
List<string> UnitOfWork.Marks() // the labels on the stack, oldest first
int UnitOfWork.EditCount() // grows with every edit — tell a pause from more editingDescription#
Mark first, then edit#
A mark records the unit of work as it is when you take it, so an edit that should be undoable on its own takes a mark first. Undo goes back to the newest mark; each press goes back one more.
entity Card {
[Required, MaxLength(40)] string Title;
security { allow create, read, update, delete when IsAuthenticated || IsAnonymous; }
}
string RenameTwiceUndoOnce() {
var c = new Card { Title = "Draft" };
UnitOfWork.Mark("Rename to Review");
c.Title = "Review";
UnitOfWork.Mark("Rename to Done");
c.Title = "Done";
UnitOfWork.Undo(); // back to the newest mark
var label = UnitOfWork.UndoLabel();
UnitOfWork.Commit(); // persists "Review" — and clears the marks
return c.Title + " / next undo: " + label; // "Review / next undo: Rename to Review"
}On a page the steps are taken for you#
A page does not have to mark at all. Each action a person runs that edits something, and each field they type into,
is a step of its own — named by the action or the field — so Undo works on any editing page with no code. A Mark
written in an action replaces that action's step and opens a group the following steps join, until the next mark,
an Undo, a commit or a discard; that is how a canvas makes one gesture one step ([[ui-page-unit-of-work#undo]]).
A function body, a workflow step and a test body take no automatic steps: there, a mark is always the author's.
How many edits has it taken?#
UnitOfWork.EditCount() grows by one with every create, field write and delete the unit of work takes, and never
goes back — an Undo's own restore does not count. Remember it and compare to tell "nothing since" from "still
editing"; it is what AutoSave waits on ([[ui-page-unit-of-work#pause]]).
entity Card {
[Required, MaxLength(40)] string Title;
security { allow create, read, update, delete when IsAuthenticated || IsAnonymous; }
}
int EditsMade() {
var c = new Card { Title = "Draft" };
var before = UnitOfWork.EditCount();
c.Title = "Review";
c.Title = "Done";
return UnitOfWork.EditCount() - before; // 2
}Undo, Redo, and what clears the redo stack#
Redo() puts back what the last Undo() (or RollbackTo) took back, and its mark with it. A new edit — a create,
a write, a delete — clears what Redo could put back, because that history was made before it. Taking a new mark
clears it too.
Rolling back to a mark you held — RollbackTo(m)#
The mark Mark() answers is a value you can keep — in a local, or in a component field — and give back later. It is a
compiler-checked value of type UnitOfWorkMark, never a label or a number: RollbackTo("Start") does not
compile.
string TryALayoutThenGoBack() {
var a = new Card { Title = "A" };
var before = UnitOfWork.Mark("Before the experiment");
a.Title = "A1";
UnitOfWork.Mark("Second step");
new Card { Title = "B" };
UnitOfWork.RollbackTo(before); // past both marks: the new card and the rename are gone
UnitOfWork.Commit();
return a.Title + ", " + Card.Count(); // "A, 1" — with what the other examples on this page committed, a count
}A mark that is no longer on the stack — undone past, rolled back past, or cleared by a commit or a discard — is refused with a sentence naming those three, and a mark from another unit of work (another page, another request) is refused rather than matched by number.
Undo and Redo buttons, with Ctrl+Z#
The reads are what an Undo button shows. They follow the stack as it moves, so a button bound to them relabels and
enables itself. Give the buttons shortcut: and the keyboard comes for free:
[Route("/cards")]
component Cards() {
live var cards = Card.OrderBy(c => c.Title);
action Add() { UnitOfWork.Mark("Add a card"); new Card { Title = "New card" }; }
action Undo() { UnitOfWork.Undo(); }
action Redo() { UnitOfWork.Redo(); }
action Save() { UnitOfWork.Commit(); } // the edits stay undoable until they are saved
render {
Stack(gap: 2) {
Row(gap: 2) {
Button("Add", onPress: Add);
Button("Save", onPress: Save, shortcut: "Mod+S");
Button(UnitOfWork.CanUndo() ? "Undo " + UnitOfWork.UndoLabel() : "Undo", onPress: Undo,
shortcut: "Mod+Z", disabled: !UnitOfWork.CanUndo());
Button(UnitOfWork.CanRedo() ? "Redo " + UnitOfWork.RedoLabel() : "Redo", onPress: Redo,
shortcut: "Mod+Shift+Z", disabled: !UnitOfWork.CanRedo());
}
foreach (var c in cards) { Text(c.Title); }
}
}
}Mod is ⌘ on a Mac and Ctrl elsewhere. Shift is part of a letter's shortcut, so Mod+Shift+Z is redo and never
read as Mod+Z.
The whole unit of work, not one level of it#
A body stages into ONE unit of work, even though a call, a try and an action each stage in a scope of their own
underneath. A mark records all of it, so:
- a mark taken in one action is undone by a LATER action (the page's unit of work outlives both);
- a mark taken inside a helper or a
try, undone after it returned, takes back only what came after the mark — what the helper or thetrydid before it is kept; - a row the undo took back is gone: a variable still holding it is refused when it is read or written, with a sentence
saying an undo took it — and
Redo()brings back the same row.
What a rollback does not touch#
- What was read. A
[ConcurrencyCheck]field remembers the value it was READ with; an undo does not change that, so a later write is still refused if somebody else changed the row meanwhile (ConcurrencyCheck). - A pending row's audit stamps are read from its being pending, so a row rolled away has none and a row brought back by Redo has them again — a rule judging a child against it sees exactly what the commit would write.
- Work already committed. A commit clears every mark; nothing reaches back past one.
- An effect that runs after the commit — ending an account's other sessions ([[stdlib-security#end-other-sessions]]), say — belongs to the edit that queued it: rolled back with it, and not run.
A write a control staged straight into the page's unit of work on the SERVER (a markdown document's sections) IS covered: its step carries a server mark, and an undo past it rolls the server's copy back too ([[ui-page-unit-of-work#server-staged]]).
Where it runs#
Everywhere the unit of work is. In a UI action the marks live on the page's unit of work in the browser — no round trip — and the reads re-render what shows them. In a server function they live on that call's unit of work and last as long as it does; a mark does not survive a hand-off between the two. The same body answers the same on both sides.
Examples#
A canvas whose every gesture is one undo step#
The flow kit — a node-and-edge canvas whose nodes are your own Osy#'s canvas raises gesture(label) once before the events of every edit — a drag, an arrow press, a
connection, a Delete — named for what it does. Marking there makes each gesture one undo step, however many rows it
moved:
action Gesture(string what) { UnitOfWork.Mark(what); } // "Move Approve?", "Remove 2 nodes and 1 edge"
…
FlowCanvas(nodes: steps, edges: links, gesture: Gesture, nodeMoved: Moved, connected: Link, edgeRemoved: Unlink)See also#
- A page's unit of work — what the platform does for an editing page, end to end — the steps a page takes for you, and the rest of an editing page's journey
- UnitOfWork — committing and discarding the whole unit of work
- The flow kit — a node-and-edge canvas whose nodes are your own Osy# — the node canvas, with step-by-step undo
- try / catch / finally — a
tryrolls back its own scope when it faults