# The conflict notice — when a page's save meets somebody else's change

> When a person's save from a page meets a row somebody else changed while they were editing it, and nothing in the app catches the ConflictException, the page shows them — per field — what they wrote and what the row holds now, and lets them keep the new value or apply theirs over it. Either choice is made knowing the value, so the save that follows is checked against what they were shown. No code to write: an app that catches the conflict decides for itself instead.

<!-- id: ui-conflict-notice · area: ui · stability: stable · html: https://osysharp.com/reference/ui/conflict-notice/ -->

## Summary        {#summary}
Two people open the same record. One saves; the other, still typing, saves after — before their page heard of the
first save. With [[entity-concurrency-check|`[ConcurrencyCheck]`]] the second save is refused rather than silently
overwriting the first — and on a page, the person whose save was refused is shown what happened instead of only being
told no:

| | Yours | Now |
|---|---|---|
| **Note** | what they typed | what the row holds now |

…with two buttons: **Keep theirs** and **Apply mine**. There is nothing to write for this; it is what a page does when
its own save loses a race and nothing in the app catches it.

## Signature      {#signature}
```osy syntax
action Save() {
  editing.Note = draft;
  UnitOfWork.Commit();     // refused when somebody else changed Note after this page last showed it
}                          // → uncaught: the page shows the conflict notice
```

## Description    {#description}

### When it appears   {#when}
All three, and only then:

- the write is the **page's own** — an action sets a member and calls `UnitOfWork.Commit()` (or the action's end
  commits it). A function the page calls on the server writes on what *that function* read and is not a page edit
  ([[entity-concurrency-check#whose-read]]);
- the member is guarded by `[ConcurrencyCheck]`, and somebody else changed it after the page last showed it. A change
  the page WAS shown — a `live` read brought it — is not a conflict: the save is based on it and lands, and a box bound to
  the value has said it changed ([[entity-concurrency-check#changed-elsewhere]]);
- nothing catches the `ConflictException`. A `try { … } catch (ConflictException e) { … }` in the action is the app
  deciding, and the notice is not shown. An app that draws `Failure` itself ([Failure — what went wrong the last time an action ran](https://osysharp.com/reference/ui/failure/)) has taken every failure
  surface over, this one included.

### What it shows   {#what}
One line per guarded member the save wrote that somebody else changed: **Yours** is what the person wrote, **Now** is
what the row holds — read for them, under their own rules, at the moment of the refusal. A member they may not read is
named but not shown ("changed — you may not see it"), and then only **Keep theirs** is offered: a value they cannot see
cannot be knowingly overwritten.

The row on the page shows the new value at once, whether or not the page reads it live.

### Keep theirs   {#keep-theirs}
The other person's value stands. Everything else the person's save carried — other fields, other rows — goes back
as pending edits, exactly as it was before they pressed Save; nothing is committed until they save again.

And the page can save that member again. Its next edit is based on the value the person was shown — so a later save
lands, and is refused only if the row moves again.

### Apply mine   {#apply-mine}
The person's whole save is put back and committed again, this time over the value they were shown. It is **not** a
blind overwrite: if the row changed a second time meanwhile, the save is refused again and the notice shows the newer
value — the person always writes over something they have seen.

### It does not cover what it is about   {#placement}
The notice sits at the foot of the screen and stays until it is answered. While it is up, the page gets room at its
foot as tall as the notice, and the row whose save was refused is scrolled clear of it — so the person can read the
row and the notice together, and every row can still be reached.

### Styling it   {#styling}
It is plain HTML with `osy-` classes, like the other built-in surfaces: `.osy-action-failed.osy-conflict` for the box,
`.osy-conflict-fields` for the table (`.osy-conflict-yours`, `.osy-conflict-theirs` for the cells),
`.osy-conflict-keep` and `.osy-conflict-apply` for the buttons. Restyle them in the app's CSS.

### With SaveBar, SaveActions or AutoSave   {#save-controls}
The kit's save controls ([Save, discard, undo — three ways to show a page's unit of work](https://osysharp.com/reference/ui/saving/)) commit the page's unit of work the same way, so a save they make that loses a
race shows this notice too — they show other refusals themselves ("Not saved — …"), and leave a conflict to it. So
does an action of your own that catches everything else and passes the conflict on:

```osy syntax
try { UnitOfWork.Commit(); }
catch (ConflictException) { throw; }                 // the notice shows it
catch (Exception e) { message = e.Message; }         // anything else, your sentence
```

### Deciding it yourself instead   {#catch}
Catch the conflict and the notice is not shown — the app's catch decides, as for any fault
([[entity-concurrency-check#abandon-or-repair]]):

```osy syntax
try { editing.Note = draft; UnitOfWork.Commit(); }
catch (ConflictException e) { UnitOfWork.Discard(); message = "Somebody changed that note — have a look first."; }
```

## Examples       {#examples}

```osy title="a page that saves its own edit, and lets the platform handle a lost race" test app=conflict-notice
[ConcurrencyCheck]
entity Recipe {
  [Required, MaxLength(60)] string Title;
  [MaxLength(200)] string? Method;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

[Route("/")]
[AllowAnonymous]
component Recipes() {
  var recipes = Recipe.OrderBy(r => r.Title);         // read once: a change made after it is a race (see below)
  Recipe? editing;
  string draft = "";

  action Edit(Recipe r) { editing = r; draft = r.Method ?? ""; }
  action Save() {
    if (editing == null) { return; }
    editing.Method = draft;
    UnitOfWork.Commit();
    editing = null;
  }

  render {
    Stack(gap: 3, p: 6) {
      foreach (var r in recipes) {
        Row(gap: 2) {
          Text($"{r.Title}: {r.Method ?? ""}");
          Button("Edit", onPress: () => Edit(r), announceAs: "Edit " + r.Title);
        }
      }
      if (editing != null) {
        Stack(gap: 2) {
          Field("Method", value: draft);
          Button("Save", onPress: Save);
        }
      }
    }
  }
}
```

```osy title="the browser and the test body are the two writers; the person applies theirs knowingly" test app=conflict-notice
[TestFixture]
void Kitchen() { new Recipe { Title = "Bread", Method = "knead" }; }

[Test(Kitchen)]
void A_lost_race_shows_both_values_and_apply_mine_lands() {
  Ui.Visit("/");
  Ui.Click("Edit Bread");
  Ui.Fill("Method", "fold, do not knead");      // the person starts, from what the page read

  var r = Recipe.Single();
  r.Method = "knead twice";
  UnitOfWork.Commit();                          // somebody else's save lands first

  Ui.Click("Save");                             // the person's save loses the race
  Assert.Visible("Somebody else changed this Recipe while you were editing it.");
  Assert.TextIs("fold, do not knead");          // Yours
  Assert.TextIs("knead twice");                 // Now

  Ui.Click("Apply mine");
  Assert.Visible("Bread: fold, do not knead");
  Assert.Equal("fold, do not knead", Recipe.Single().Method);
}

[Test(Kitchen)]
void Keep_theirs_leaves_their_value_and_the_page_can_save_it_again() {
  Ui.Visit("/");
  Ui.Click("Edit Bread");
  Ui.Fill("Method", "fold");

  var r = Recipe.Single();
  r.Method = "knead twice";
  UnitOfWork.Commit();

  Ui.Click("Save");
  Ui.Click("Keep theirs");
  Assert.Equal("knead twice", Recipe.Single().Method);

  Ui.Click("Edit Bread");
  Ui.Fill("Method", "knead twice, then rest");
  Ui.Click("Save");
  Assert.Visible("Bread: knead twice, then rest");
}
```

```osy title="a page kept with SaveBar loses a race, and the person still chooses" test app=conflict-notice-savebar
using Osysharp.Ui;

[ConcurrencyCheck]
entity Lamp {
  [Required, MaxLength(60)] string Name;
  [MaxLength(200)] string? Fuel;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

[Route("/lamps/{id}")]
[Render(CSR)]
[AllowAnonymous]
component LampPage(Guid id) {
  var lamp = Lamp.FirstOrDefault(p => p.Id == id);   // read once, so a save after another one is a race
  render {
    if (lamp != null) {
      Stack(gap: 3, p: 6) {
        Text(lamp.Name + ": " + (lamp.Fuel ?? ""));
        Field("Fuel", value: lamp.Fuel);
        SaveBar(saved: "Kept.");
      }
    }
  }
}

[Test]
void A_lost_race_under_a_save_bar_shows_both_values() {
  var hall = new Lamp { Name = "Hall lamp", Fuel = "paraffin" };
  UnitOfWork.Commit();
  Ui.Visit("/lamps/" + hall.Id);
  Ui.Fill("Fuel", "olive oil");                // the person starts, from what the page read

  var other = Lamp.Single();
  other.Fuel = "lamp oil";
  UnitOfWork.Commit();                          // somebody else's save lands first

  Ui.Click("Save");
  Assert.Visible("Somebody else changed this Lamp while you were editing it.");
  Assert.TextIs("olive oil");                  // Yours
  Ui.Click("Apply mine");
  Assert.Equal("olive oil", Lamp.Single().Fuel);
}
```

## See also       {#see-also}
- [ConcurrencyCheck](https://osysharp.com/reference/entity/concurrency-check/) — the attribute, and who the writer is when a page calls a server function
- [Failure — what went wrong the last time an action ran](https://osysharp.com/reference/ui/failure/) — the other built-in failure surface, and how an app takes it over
- [UnitOfWork](https://osysharp.com/reference/function/unit-of-work/) — the page's unit of work, and what a commit carries
- [try / catch / finally](https://osysharp.com/reference/function/try-catch/) — catching the conflict to decide yourself
