Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

UnitOfWork savepoints — Mark, Undo, Redo, RollbackTo

UnitOfWork.Mark(label) · UnitOfWork.Undo() · UnitOfWork.Redo() · UnitOfWork.RollbackTo(mark) · UnitOfWork.CanUndo() · CanRedo() · UndoLabel() · RedoLabel() · Marks()

Marks form a stack on the unit of work. UnitOfWork.Mark(label) records everything staged so far and answers a UnitOfWorkMark; Undo() rolls back to the newest mark and pops it (Ctrl+Z); Redo() re-applies what the last Undo took back until a new edit clears it; RollbackTo(m) jumps back past every later mark. Creates, updates, deletes and collection membership all roll back; nothing is sent anywhere until a commit, which clears the marks.

preview4 examples compiled by CIfunctiondataauthoringui

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 editing

Description#

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 the try did 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#

Related

UnitOfWork

Every body accumulates its writes in a unit of work rather than sending them one at a time. UnitOfWork.Commit() makes…

The flow kit — a node-and-edge canvas whose nodes are your own Osy#

A modelling canvas — pan, zoom, drag nodes, draw edges between ports, select and delete, by mouse and by keyboard —…

try / catch / finally

Handles a fault. C#'s syntax, including typed catches, catch filters and finally. The rows written inside a try block…

Save, discard, undo — three ways to show a page's unit of work

Every edit on a page joins the route's unit of work; how the page OFFERS to keep it is a choice of presentation…