Skip to content

A reloaded ISerializableCommand loses its NavigationContext and ChangeMetadata, so UndoAsync/RedoAsync no longer navigate after LoadStateAsync #94

Description

@matt-edmondson

What's wrong

JsonUndoRedoSerializer writes NavigationContext and Metadata for every command. On load (ConvertFromSerializableCommand in UndoRedo/Services/JsonUndoRedoSerializer.cs), only the PlaceholderCommand fallback reads them back.

A command that implements ISerializableCommand is rebuilt with Activator.CreateInstance(commandType) followed by DeserializeData(data). It keeps whatever its parameterless constructor set:

  • NavigationContext is null;
  • ChangeMetadata is the default (size 1, timestamp = load time, and so on).

So a serializable command, the only kind that survives a round trip as a real, undoable command, is the one that loses its navigation and metadata.

Failure scenario

sealed class SetText : BaseCommand, ISerializableCommand
{
    public string Text = "";
    public SetText() : base(ChangeType.Modify, ["doc"]) { }
    public SetText(string t, string nav, int size) : base(ChangeType.Modify, ["doc"], nav, size) { Text = t; }
    public override string Description => $"Set {Text}";
    public override void Execute() { }
    public override void Undo() { }
    public string SerializeData() => Text;
    public void DeserializeData(string d) => Text = d;
}

source.Execute(new SetText("hello", "line:42", 7));
string data = await source.SaveStateAsync();
await target.LoadStateAsync(data);   // target has a navigation provider set
await target.UndoAsync();
  • Actual: after the load the command is a SetText with NavigationContext == null, change size 1 and a timestamp equal to the load time. UndoAsync navigates nowhere.
  • Expected: NavigationContext == "line:42", the size and timestamp are preserved, and UndoAsync navigates to line:42, as it does before the save.

In an editor, this means undo/redo stops jumping to the edited location after the app is restarted and history is restored. That is the main point of persisting navigation context.

This is related to #91, which covers save-boundary timestamps, but it is a separate gap: per-command navigation and metadata are dropped on reconstruction.

Suggested fix

Add an internal hook to BaseCommand, such as RestoreSerializedState(string? navigationContext, ChangeMetadata metadata). The relevant setters are already protected set. Call it in ConvertFromSerializableCommand after DeserializeData, whenever the instance is a BaseCommand.

Acceptance criteria

  • A round-trip test through SaveStateAsync/LoadStateAsync for an ISerializableCommand preserves NavigationContext and Metadata (change type, affected items, size, timestamp).
  • After the load, UndoAsync calls the navigation provider with the original context.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreadyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions