# UnitOfWork savepoints — Mark, Undo, Redo, RollbackTo

> 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.

<!-- id: function-unit-of-work-savepoints · area: function · stability: preview · html: https://osysharp.com/reference/function/unit-of-work-savepoints/ -->

## Summary        {#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      {#signature}

```osy syntax
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    {#description}

### Mark first, then edit   {#mark-first}

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.

```osy title="each rename is one step back" test app=function-unit-of-work-savepoints
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   {#page-steps}

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?   {#edit-count}

`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]]).

```osy title="two writes, two edits" test app=function-unit-of-work-edit-count
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}

`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)`   {#rollback-to}

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.

```osy title="try something, and step back past everything it did" test app=function-unit-of-work-savepoints
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   {#buttons}

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:

```osy title="an undo button that says what it undoes" test app=function-unit-of-work-savepoints
[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   {#chain}

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   {#not-touched}

- **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](https://osysharp.com/reference/entity/concurrency-check/)).
- **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   {#execution-side}

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       {#examples}

### A canvas whose every gesture is one undo step   {#example-canvas}

[The flow kit — a node-and-edge canvas whose nodes are your own Osy#](https://osysharp.com/reference/ui/flow-kit/)'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:

```osy title="one mark per gesture" syntax
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       {#see-also}

- [A page's unit of work — what the platform does for an editing page, end to end](https://osysharp.com/reference/ui/page-unit-of-work/) — the steps a page takes for you, and the rest of an editing page's journey
- [UnitOfWork](https://osysharp.com/reference/function/unit-of-work/) — committing and discarding the whole unit of work
- [The flow kit — a node-and-edge canvas whose nodes are your own Osy#](https://osysharp.com/reference/ui/flow-kit/) — the node canvas, with step-by-step undo
- [try / catch / finally](https://osysharp.com/reference/function/try-catch/) — a `try` rolls back its own scope when it faults
