Atc.UndoRedo
1.1.5
dotnet add package Atc.UndoRedo --version 1.1.5
NuGet\Install-Package Atc.UndoRedo -Version 1.1.5
<PackageReference Include="Atc.UndoRedo" Version="1.1.5" />
<PackageVersion Include="Atc.UndoRedo" Version="1.1.5" />
<PackageReference Include="Atc.UndoRedo" />
paket add Atc.UndoRedo --version 1.1.5
#r "nuget: Atc.UndoRedo, 1.1.5"
#:package Atc.UndoRedo@1.1.5
#addin nuget:?package=Atc.UndoRedo&version=1.1.5
#tool nuget:?package=Atc.UndoRedo&version=1.1.5
Atc.UndoRedo
A production-grade, platform-agnostic undo/redo framework for .NET.
๐ Table of contents
- โจ Features
- ๐ฆ Installation
- ๐ Quick start
- ๐งฉ Core concepts
- ๐ ๏ธ Usage examples
- ๐งช Samples
- โ Testing
- ๐๏ธ Project structure
- ๐ Changelog
- ๐ค Contributing
- ๐ License
โจ Features
Core undo/redo:
- Dual-stack undo/redo with configurable history limit
- Thread-safe via
ReaderWriterLockSlim - Pure .NET with zero platform dependencies
Command composition:
- Command grouping for atomic multi-command transactions
- Command merging to coalesce rapid changes (slider drags, typing)
- Property tracking via delegate-based
PropertyChangeCommand<T> - Rich commands carrying metadata, timestamps, and contexts
Advanced history control:
- Named snapshots for save points in history
- History serialization to persist and restore undo/redo state
- Audit logging with timestamps for all operations
- Non-linear history with branching redo paths
- Operation approval to veto undo/redo operations
- Memory-aware trimming with a configurable memory budget
๐ฆ Installation
Install the NuGet package:
dotnet add package Atc.UndoRedo
Or add it manually to your .csproj:
<PackageReference Include="Atc.UndoRedo" Version="1.0.0" />
Target framework: net10.0.
๐ Quick start
using Atc.UndoRedo.Commands;
using Atc.UndoRedo.Services;
var list = new List<string>();
var service = new UndoRedoService();
service.Execute(new UndoCommand(
description: "Add item",
execute: () => list.Add("Hello"),
unExecute: () => list.Remove("Hello")));
service.Undo(); // list is empty
service.Redo(); // list contains "Hello"
๐งฉ Core concepts
IUndoCommand / UndoCommand
The fundamental command contract (Description, Execute(), UnExecute()). UndoCommand is a delegate-based implementation for the common case. See src/Atc.UndoRedo/Interfaces/IUndoCommand.cs and src/Atc.UndoRedo/Commands/UndoCommand.cs.
PropertyChangeCommand<T>
A pre-built command for reverting a single property change from oldValue to newValue. See src/Atc.UndoRedo/Commands/PropertyChangeCommand.cs.
UndoCommandGroup
Wraps a list of commands into a single atomic undo unit. Forward order on execute, reverse order on un-execute. See src/Atc.UndoRedo/Commands/UndoCommandGroup.cs.
IMergeableUndoCommand
Extends IUndoCommand with a MergeId and TryMergeWith(...) so consecutive compatible commands (typing, slider drags) collapse into one undo step. See src/Atc.UndoRedo/Interfaces/IMergeableUndoCommand.cs.
IRichUndoCommand / RichUndoCommand
Commands that carry extra metadata (id, timestamp, parameter, data, image, user-action flag, contexts). See src/Atc.UndoRedo/Commands/RichUndoCommand.cs.
UndoRedoService
The orchestrator. Exposes Execute, Undo, Redo, Clear, BeginGroup, SuspendRecording, snapshots, serialization, approvers, events, and CanUndo/CanRedo state. See src/Atc.UndoRedo/Services/UndoRedoService.cs.
๐ ๏ธ Usage examples
Command grouping
BeginGroup returns an IDisposable scope. Any command executed inside the scope is collapsed into a single undo step when the scope disposes.
using (service.BeginGroup("Rename and move"))
{
service.Execute(renameCommand);
service.Execute(moveCommand);
}
service.Undo(); // reverts both in one step
Property tracking
var person = new Person { Name = "Ada" };
var oldName = person.Name;
var newName = "Grace";
service.Execute(new PropertyChangeCommand<string>(
description: "Rename person",
setter: v => person.Name = v,
oldValue: oldName,
newValue: newName));
Command merging
Implement IMergeableUndoCommand; successive commands with the same MergeId can fold into one.
public sealed class SliderChangeCommand : IMergeableUndoCommand
{
public string Description => "Slider change";
public int MergeId => 42;
public bool IsObsolete => false;
public void Execute() { /* apply newValue */ }
public void UnExecute() { /* restore oldValue */ }
public bool TryMergeWith(IUndoCommand other)
=> other is SliderChangeCommand next && /* update newValue from next */ true;
}
Named snapshots
var snapshot = service.CreateSnapshot("Before refactor");
// ... user performs several actions ...
service.RestoreSnapshot(snapshot); // jumps history back to the snapshot
History serialization
Commands that should survive a save must implement ISerializableUndoCommand. Provide an IUndoCommandDeserializer on load.
using var fs = File.Create("history.bin");
service.SaveHistory(fs);
using var fsIn = File.OpenRead("history.bin");
service.LoadHistory(fsIn, myDeserializer);
Audit logging
var logger = new UndoRedoAuditLogger();
var service = new UndoRedoService { AuditLogger = logger };
service.Execute(command);
foreach (var entry in logger.Entries)
{
Console.WriteLine($"{entry.Timestamp:O} {entry.ActionType} {entry.Description}");
}
Operation approval
public sealed class ConfirmDestructive : IUndoOperationApprover
{
public bool ApproveUndo(IUndoCommand command) => Prompt(command);
public bool ApproveRedo(IUndoCommand command) => Prompt(command);
private static bool Prompt(IUndoCommand c) => /* show dialog */ true;
}
service.RegisterApprover(new ConfirmDestructive());
Memory budget
MaxHistorySize caps command count; MaxHistoryMemory (bytes) caps estimated memory. Commands that implement IMemoryAwareUndoCommand contribute their EstimatedMemoryBytes to the budget.
var service = new UndoRedoService
{
MaxHistorySize = 500,
MaxHistoryMemory = 64 * 1024 * 1024, // 64 MiB
};
๐งช Samples
Three sample projects live under sample/:
Atc.UndoRedo.Sample.BlazorServerโ Blazor Server host. Run withdotnet run --project sample/Atc.UndoRedo.Sample.BlazorServerand open https://localhost:18774.Atc.UndoRedo.Sample.BlazorWasmโ Blazor WebAssembly host. Run withdotnet run --project sample/Atc.UndoRedo.Sample.BlazorWasmand open https://localhost:18775.Atc.UndoRedo.Sample.SharedDemoโ Razor class library shared by both hosts; contains the demo pages.
The demo pages cover:
BasicUndoRedoโ coreExecute/Undo/Redoover a list.CommandGroupingโ multi-command atomic transactions.MergeableCommandsโ coalescing rapid changes into one undo step.PropertyTrackingโ property-level change recording.HistoryViewerโ inspecting the undo and redo stacks live.
โ Testing
Tests use xUnit v3 and live under test/Atc.UndoRedo.Tests. Run the full suite from the repository root:
dotnet test
๐๏ธ Project structure
atc-undoredo/
โโโ src/
โ โโโ Atc.UndoRedo/ # The library
โโโ sample/
โ โโโ Atc.UndoRedo.Sample.BlazorServer/
โ โโโ Atc.UndoRedo.Sample.BlazorWasm/
โ โโโ Atc.UndoRedo.Sample.SharedDemo/
โโโ test/
โ โโโ Atc.UndoRedo.Tests/ # xUnit v3 test suite
โโโ Atc.UndoRedo.slnx
โโโ Directory.Build.props # Shared build config, analyzers, versioning
โโโ CHANGELOG.md
โโโ README.md
๐ Changelog
See CHANGELOG.md. The project follows Keep a Changelog and Semantic Versioning, with releases driven by release-please.
๐ค Contributing
Issues and pull requests are welcome. Please open a GitHub issue to discuss larger changes before submitting a PR.
๐ License
Licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Atc.UndoRedo:
| Package | Downloads |
|---|---|
|
Atc.Wpf.Controls
WPF control library providing atomic UI controls including input controls, pickers, layouts, and progress indicators. |
|
|
Atc.Wpf.UndoRedo
WPF undo/redo UI components (history view, keyboard behavior) on top of the Atc.UndoRedo service. |
GitHub repositories
This package is not used by any popular GitHub repositories.