Building a Blazor Form with the Command Pattern and Undo
In a typical Blazor form, inputs update the model directly. That is simple and works fine for basic CRUD screens, but it starts to feel limiting when you want more control over how changes happen, especially if you want undo support.
That is where the Command Pattern becomes useful.
Instead of letting the form change the model directly, each meaningful edit is wrapped inside a command object. That command is responsible for two things: applying the change and reversing it later if the user clicks Undo.
In other words, the form is no longer just changing values. It is recording actions.
The basic idea
In this approach, every field change becomes its own small object.
So if the user changes FirstName, that is one command.
If they change Email, that is another command.
If they toggle IsActive, that is a third command.
Those commands are stored in a stack, and Undo simply reverses the most recent one.
The core interface is very small:
public interface IUndoableCommand
{
string Name { get; }
void Execute();
void Undo();
}
This keeps the pattern easy to understand. Every command knows what it does, and how to undo it.
The undo manager
To store the history of changes, we use a simple UndoManager:
public sealed class UndoManager
{
private readonly Stack<IUndoableCommand> _undoStack = new();
public bool CanUndo => _undoStack.Count > 0;
public void Execute(IUndoableCommand command)
{
command.Execute();
_undoStack.Push(command);
}
public void Undo()
{
if (_undoStack.Count == 0)
return;
var command = _undoStack.Pop();
command.Undo();
}
public void Clear()
{
_undoStack.Clear();
}
}
The idea is straightforward. When a command runs, it is pushed onto the stack. When the user clicks Undo, the last command is popped and reversed.
A reusable command for field changes
For a form, a generic SetPropertyCommand is often enough to get started.
public sealed class SetPropertyCommand<TModel, TValue> : IUndoableCommand
{
private readonly TModel _model;
private readonly Func<TModel, TValue> _getter;
private readonly Action<TModel, TValue> _setter;
private readonly TValue _newValue;
private TValue? _oldValue;
private bool _captured;
public string Name { get; }
public SetPropertyCommand(
string name,
TModel model,
Func<TModel, TValue> getter,
Action<TModel, TValue> setter,
TValue newValue)
{
Name = name;
_model = model;
_getter = getter;
_setter = setter;
_newValue = newValue;
}
public void Execute()
{
if (!_captured)
{
_oldValue = _getter(_model);
_captured = true;
}
_setter(_model, _newValue);
}
public void Undo()
{
if (!_captured)
return;
_setter(_model, _oldValue!);
}
}
What makes this useful is that it does not permanently track the whole form. It only represents one change at a time.
That is an important distinction.
SetPropertyCommand is not a background tracker watching every field. It is more like a history entry. One command instance represents one action, like:
change FirstName from "Maria" to "Anna"
change Email from "old@mail.com" to "new@mail.com"
change IsActive from false to true
Example from the form
In the Blazor form, a field change is turned into a command like this:
ExecutePropertyCommand(
"Change First Name",
m => m.FirstName,
(m, v) => m.FirstName = v,
value ?? string.Empty,
nameof(CustomerEditModel.FirstName));
This is a nice example because it shows exactly what the command needs:
how to read the property
how to set the property
what the new value is
which field changed
Then inside ExecutePropertyCommand, the actual command is created:
var command = new SetPropertyCommand<CustomerEditModel, TValue>(
name,
_model,
getter,
setter,
newValue);
And then executed through the undo manager:
_undoManager.Execute(command);
Why this feels cleaner
This approach separates form editing from raw property assignment.
Without the pattern, a form usually does this:
_model.FirstName = value;
That works, but the change is gone the moment it happens. There is no history, no reversible action, and no good place to add undo logic later.
With the Command Pattern, the change becomes a first-class action. That makes the form behavior more predictable, easier to test, and easier to extend.
Why Save should stay separate
One important design choice is keeping Save outside the command stack.
Undo should reverse form edits, not database persistence.
That is why Save is better as a separate action:
private Task SaveAsync()
{
ValidateModel();
if (!_editContext.Validate())
return Task.CompletedTask;
// persist here
_undoManager.Clear();
RefreshPreview();
return Task.CompletedTask;
This keeps responsibilities clear:
commands handle reversible UI changes
Save persists the current state
Trying to make Save itself a command usually creates confusion, because undoing a database write is a completely different problem.
A good fit for Blazor forms
This pattern is especially useful when a form has:
multiple editable fields
toggles or switches
add/remove actions
sections that can be reset
a real requirement for undo
The Command Pattern gives you a clean way to implement it without scattering logic all over the component.
Final thought
The most helpful way to think about this is not that the command is โtracking fields.โ It is not.
What it is really doing is turning every meaningful edit into a small object that remembers what changed, so the form can step backward when needed.
That is what makes undo possible, and that is what makes the pattern useful in a Blazor form.
Source Code : https://github.com/stevsharp/BlazorCommandLab
Top comments (0)