The Memento pattern is one of the foundational design patterns. It helps build UI features like undo/redo or UI transactions. In this article, we will see how to implement the Memento pattern in C#. We use a small WPF-based Fish Tank Manager as a sample application.
What is the Memento design pattern?
The Memento design pattern is a software design pattern that provides the ability to restore an object to its previous state without revealing its internal structure. The aim is to provide standardization in scenarios where the object’s state needs to be saved and restored, such as the GUI undo/redo mechanism we presented earlier, but also for, for example, in-memory transactions.
The Memento pattern consists of three key components:
- Originator : The object whose state needs to be saved and restored.
- Memento : An object that contains a snapshot of the Originator’s state.
- Caretaker : The object that keeps track of multiple mementos, often implementing mechanisms to store and retrieve them.
While using the Memento pattern, two processes occur:
- Capture : When a state snapshot is needed, the Originator creates a Memento object and passes it to the Caretaker for safekeeping.
- Restore : To restore the state, the Caretaker provides the stored Memento back to the Originator, which rewrites its state.
When to use the Memento pattern?
Here are some situations when you might use the Memento pattern:
Undo/Redo Functionality : When you need to implement undo and redo mechanisms, the Memento pattern is ideal. It allows you to save the state of an object at a certain point and revert back to it when needed. For example, in text editors, graphic editors, or games.
State History or Snapshot Tracking : If an application requires keeping a history of different states of an object, the Memento pattern can be useful. This is common in version control systems or in any system where you need to track changes over time.
Transaction Management : In applications where transactions are used (like database operations), the Memento pattern can help in saving the state before the transaction starts. If the transaction fails, the state can be restored to its original state.
When introduced in an application, the Memento pattern must be exhaustively implemented in the whole object model for the user experience to be consistent. This means significant implementation and maintenance costs.
Example: Fish in a Fish Tank
Imagine you are implementing an application that allows for tracking fish in your home fish tank.
The application consists of a list of fish, allowing you to add a fish, remove a fish, and edit a fish.
Let’s see how it looks, with its beatiful Undo button:
The app, which is implemented in WPF, initially has no functionality for undoing changes. We will add this as the article progresses. In particular, the Fish
class, which represents a single fish, looks like this:
public sealed class Fish : ObservableRecipient
{
private string? _name;
private string? _species;
private DateTime _dateAdded;
public string? Name
{
get => this._name;
set => this.SetProperty( ref this._name, value, true );
}
public string? Species
{
get => this._species;
set => this.SetProperty( ref this._species, value, true );
}
public DateTime DateAdded
{
get => this._dateAdded;
set => this.SetProperty( ref this._dateAdded, value, true );
}
}
As you can see, the class is initially pretty simple and contains only the data fields.
Let’s also look at the data fields of the MainViewModel
class, which we will change later:
private readonly IFishGenerator _fishGenerator;
private bool _isEditing;
private Fish? _currentFish;
public ObservableCollection<Fish> Fishes { get; } = new();
How to implement the Memento pattern in C#?
The Memento pattern is implemented by defining interfaces for the Memento, Originator, and Caretaker:
The
IMementoable
interface is implemented by originator objects that hold state needing to be captured and restored. It allowsIMementoCaretaker
to retrieve memento objects and restore the state of the object.The
IMemento
interface is minimalistic: it just exposes anOriginator
property used by theIMementoCaretaker
during the Undo process.The
IMementoCaretaker
is a service that manages storedIMemento
instances and uses them to restore states ofIMementoable
objects.
In C#, it’s best to define the implementation of the IMemento
interface as a private nested class. This stays true to the primary goal of the Memento pattern to respect encapsulation and allow the memento to access the private state of the originator.
A straightforward approach involves manually implementing the IMementoable
and IMemento
interfaces. In our example, this involves declaring a Memento
nested type in both MainViewModel
and Fish
.
Step 1: Implement the Caretaker class
First, we implement the Caretaker
class that implements the IMementoCaretaker
interface. Our implementation is very simple: just a stack of memento objects.
public sealed class MementoCaretaker : IMementoCaretaker
{
private readonly Stack<IMemento> _mementos = new();
public void CaptureSnapshot( IMementoable mementoable )
{
this._mementos.Push( mementoable.SaveToMemento() );
}
public void Undo()
{
if ( this._mementos.Count > 0 )
{
var memento = this._mementos.Pop();
memento.Originator.RestoreMemento( memento );
}
}
public bool CanUndo => this._mementos.Count > 0;
}
Step 2: Register the Caretaker service
Conceptually, the Caretaker is often a Singleton. In a modern .NET app, it’s a good practice to use appBuilder.Services.AddSingleton
to register it.
services.AddSingleton<IMementoCaretaker, MementoCaretaker>();
Step 3: Implement the IMementoable interface
Here is the most boring and repetitive part. We must implement the IMementoable
interface for all classes that hold undoable state. In each of these classes, we create the following artifacts:
- A private nested
Memento
with the following members:- For each mutable field or property of the originator class, a public field of the same name and type.
- An implementation of the
IMemento
interface, specifically theOriginator
property. - A constructor that stores the fields and properties of the originator class into the properties of the
Memento
class, as well as theOriginator
property.
- An implementation of the
IMementoable
interface including:- The
SaveToMemento
method, returning an instance of theMemento
nested class. - The
RestoreMemento
method, copying the memento properties back into the originator’s fields and properties.
- The
Let’s implement this pattern into the Fish
class:
public sealed class Fish : ObservableRecipient, IMementoable
{
private string? _name;
private string? _species;
private DateTime _dateAdded;
public string? Name
{
get => this._name;
set => this.SetProperty( ref this._name, value, true );
}
public string? Species
{
get => this._species;
set => this.SetProperty( ref this._species, value, true );
}
public DateTime DateAdded
{
get => this._dateAdded;
set => this.SetProperty( ref this._dateAdded, value, true );
}
public void RestoreMemento( IMemento memento )
{
if ( memento is not Memento s )
{
throw new InvalidOperationException( "Invalid snapshot." );
}
this.Name = s.Name;
this.Species = s.Species;
this.DateAdded = s.DateAdded;
}
public IMemento SaveToMemento()
{
return new Memento( this, this.Name,
this.Species, this.DateAdded );
}
private sealed record Memento(
Fish Originator,
string? Name,
string? Species,
DateTime DateAdded ) : IMemento
{
IMementoable IMemento.Originator => this.Originator;
}
}
Step 4: Coping with collection types
When implementing the Memento pattern, ensure that all mutable classes implement it. For collections, you have two options: either implement IMementoable
collections or use immutable collections. In this example, we use the second approach. In MainViewModel
, we trade our ObservableCollection<Fish>
for an ImmutableList<Fish>
. Using ImmutableList
instead of, say, an ImmutableArray
, is a good choice because ImmutableList
tries to reuse as much memory as possible between different edits of the list, while ImmutableArray
would always create a new copy.
Let’s see how the data fields of MainViewModel
have changed:
private readonly IMementoCaretaker? _caretaker;
private readonly IFishGenerator _fishGenerator;
private bool _isEditing;
private Fish? _currentFish;
private ImmutableList<Fish> _fishes = ImmutableList<Fish>.Empty;
Also, see the Memento
class for MainViewModel
:
private class Memento : IMemento
{
public IMementoable Originator { get; }
public bool IsEditing { get; }
public Fish? CurrentItem { get; }
public ImmutableList<Fish> Items { get; }
public Memento(
MainViewModel snapshotable,
bool isEditing,
Fish? currentItem,
ImmutableList<Fish> items )
{
this.Originator = snapshotable;
this.IsEditing = isEditing;
this.CurrentItem = currentItem;
this.Items = items;
}
}
Step 5: Calling CaptureMemento and Undo
So far, we have only implemented the IMementoable
pattern, and we should start using it to add functionality to the application.
An inconvenience of the Memento pattern is that the IMementoCaretaker.CaptureMemento
method must be called manually. If you’re implementing an undo/redo feature, you would typically call it before any operation.
private void ExecuteNew()
{
this._caretaker?.CaptureSnapshot( this );
this.Fishes = this.Fishes.Add(
new Fish()
{
Name = this._fishGenerator.GetNewName(),
Species = this._fishGenerator.GetNewSpecies(),
DateAdded = DateTime.Now
} );
}
If you’re still using modal dialogs with a transactional user interface based on the Ok and Cancel buttons, you can capture a memento when the dialog opens, and call Undo if the user hits Cancel.
For instance, here is the code that opens the UI in edit mode (ExecuteEdit
) and handles the OK (ExecuteSave
) and Cancel (ExecuteCancel
) buttons.
private void ExecuteEdit()
{
this.IsEditing = true;
this._caretaker?.CaptureSnapshot( this._currentFish! );
}
private void ExecuteSave()
{
this.IsEditing = false;
}
private void ExecuteCancel()
{
this.IsEditing = false;
this._caretaker?.Undo();
}
As you can see, the Memento pattern can be used to implement UI transactions.
How to implement Memento without boilerplate?
As you can judge from Step 3 above, implementing Memento types follows a very repetitive pattern. Each Memento type must declare fields that correspond to the target class, create a similar constructor, implement the same interface, etc. There is virtually no reason to implement this pattern by hand. Adopting a code generation technology saves both implementation time and maintenance costs. And a lot of gray hairs, because a code generator, unlike a human, won’t forget to add a property to the Memento when one is added to the Originator!
The .NET community offers several code generation technologies which can be grouped into two categories: run-time and compile-time generation. Run-time generation should be avoided because it adds a startup performance overhead and is difficult to debug. Within the second category, you have two options: source generators like Roslyn source generators or Metalama, and, historically, MSIL generators like Fody. Fody and Roslyn source generators are very low-level and tend to be overly complex. Metalama, on the other hand, offers a simpler API and development experience. Metalama is free to use. It is based on Roslyn and actually uses Roslyn source generators under the hood.
With Metalama, you can reduce the pattern to a single attribute, MementoAttribute
, called an aspect. This aspect generates the Memento code on-the-fly during compilation, keeping your source code clean and succinct. The aspect automatically performs all the steps we mentioned above. Going through all implementation steps of this aspect is beyond the scope of this article. We refer you to The Memento Pattern in Metalama documentation for details.
Metalama also comes with other open-source aspects, such as Observable, which automatically implements the INotifyPropertyChanged
interface, and WPF Command.
When we use these aspects in our project, the Fish
class gets reduced to its essence:
[Memento]
[Observable]
public sealed partial class Fish
{
public string? Name { get; set; }
public string? Species { get; set; }
public DateTime DateAdded { get; set; }
}
The code that handles IMementoable
and INotifyPropertyChanged
is no longer necessary in your source code because it is generated during compilation. So, we’ve saved work and, at the same time, improved the reliability and maintainability of the code. If we have 50 classes implementing the Memento pattern, we still have just one MementoAttribute
to maintain.
How to automatically capture snapshots upon changes?
You’re certainly aware that there are two approaches to user interfaces for data entry. One is based on explicit Ok and Cancel buttons. The second auto-saves any change. In this case, a good undo/redo feature is almost mandatory.
If you are building an auto-save user experience, it’s a good idea to trigger the call to CaptureMemento
whenever a property changes.
If your object model already implements INotifyPropertyChanged
, it’s reasonably easy to subscribe to the PropertyChanged
event and call CaptureMemento
upon any change. For instance, we added this logic to FishControl.xaml.cs
. If you have several windows or controls, you can move this logic to a base class, or even to an aspect if you’re using Metalama.
protected override void OnPropertyChanged( DependencyPropertyChangedEventArgs e )
{
base.OnPropertyChanged( e );
if ( e.Property.Name == nameof(this.DataContext) )
{
if ( e.OldValue is IMementoable and INotifyPropertyChanged newObservable )
{
newObservable.PropertyChanged -= this.OnMementoablePropertyChanged;
}
if ( e.NewValue is IMementoable newMementoable and INotifyPropertyChanged newObervable )
{
newObervable.PropertyChanged += this.OnMementoablePropertyChanged;
// Capture _before_ any change.
this._caretaker?.CaptureMemento( newMementoable );
}
}
}
private void OnMementoablePropertyChanged( object? sender, PropertyChangedEventArgs e )
{
var mementoable = (IMementoable?) this.DataContext;
if ( mementoable != null )
{
this._caretaker?.CaptureMemento( mementoable );
}
}
In WPF, this approach will save a memento whenever the user focus switches from one control to another.
If your user is expected to type long texts, you might want to save the state more often. In this case, you can cause the binding to be updated on every keystroke. You can do this by hooking the KeyUp
event.
<TextBox Grid.Row="0" Grid.Column="1" Margin="2"
Text="{Binding Name}" KeyUp="OnTextBoxUpdated" />
private void OnTextBoxUpdated( object sender, KeyEventArgs e )
{
if ( sender is TextBox textBox )
{
var binding = BindingOperations.GetBindingExpression(
textBox, TextBox.TextProperty );
if ( binding != null )
{
binding.UpdateSource();
}
}
}
However, this strategy creates a new snapshot for every keystroke, which is highly inconvenient both for the user and for memory usage.
To mitigate this problem, the Caretaker can ignore a new memento if the last memento is recent and of identical originator.
Modify the
IMemento
interface to this:Modify the implementation pattern to set the value of the
MementoTime
property toDateTime.Now
.Change
Caretaker.CaptureMemento
to ignore requests that are too close to each other.
Now, you will have a savepoint every 5th second at most for each object.
How to implement IEditableObject using the Memento pattern
The IEditableObject
is the system interface for editable objects that support Ok-Cancel semantics. It is used by controls such as DataGrid
.
public interface IEditableObject
{
void BeginEdit();
void EndEdit();
void CancelEdit();
}
It’s easy to implement IEditableObject
if your class already implements IMementoable
. Here is an example.
public abstract partial class EditableObject : IEditableObject, IMementoable
{
private IMemento? _beforeEditMemento;
public void BeginEdit()
{
if ( this._beforeEditMemento != null )
{
throw new InvalidOperationException();
}
this._beforeEditMemento = this.SaveToMemento();
}
public void CancelEdit()
{
if ( this._beforeEditMemento == null )
{
throw new InvalidOperationException();
}
this.RestoreMemento( this._beforeEditMemento );
}
public void EndEdit()
{
if ( this._beforeEditMemento == null )
{
throw new InvalidOperationException();
}
this._beforeEditMemento = null;
}
public abstract IMemento SaveToMemento();
public abstract void RestoreMemento( IMemento memento );
}
What are the common problems of the Memento pattern?
The Memento pattern is very useful. However, when deciding whether to use it and how to implement it in detail, there are some challenges to keep in mind.
Problem 1. Working with object graphs
The Memento pattern has been formalized to take snapshots of a single class instance. However, in most practical applications, we will work not with isolated object instances but with object graphs. And we will want to take a memento of the whole graph.
This problem is not addressed by the classic Memento pattern, and we see that as its major limitation.
If we want to follow the Memento pattern with multi-object operations, we will need to call the CaptureMemento
method for all objects in the operation. We will need to improve the Caretaker to support multi-object undo operations.
void CaptureMemento( IEnumerable<IMementoable> originators );
It’s important that all mementos captured in a single call of CaptureMemento
are also restored in a single call of Undo
.
Problem 2. Memory consumption
Storing a large number of mementos can lead to significant memory usage. This is especially true for collections.
One solution is to selectively save only the necessary information, such as changes or deltas, especially on large objects. This can significantly reduce memory usage but is very challenging to implement. This solution, however, deviates from the pure Memento pattern.
Another approach is limiting the available history to a reasonable number of changes by removing the oldest mementos when the history contains more than this limit.
Problem 3. Working with collections
Dealing with mutable collections within the Originator’s state requires careful handling to prevent inconsistencies. Simply copying the data field will result in a reference to the same collection, which may be shared between the live state and multiple mementos. This violates the purpose of the Memento pattern.
There are two ways to address this issue:
- Deep copy : The collection can be copied when the Memento is being captured. The problem with this approach is very high memory consumption.
- Immutable collections : The target class uses fully immutable collections, which are remembered by the memento as normal values.
In the sample, we have used the second approach. While there are some performance and memory costs associated with immutable collections, it’s the simplest way of working with collections with the Memento pattern.
Problem 4. State isolation during edits
Last and not least, a major drawback of the Memento pattern is that user edits will affect the “good” instance of the object instead of a “draft” one. If your application has background tasks, they will see unconfirmed changes, that were not yet saved by clicking the Ok button. Writing temporary edits to a “good” instance can create serious problems because neither your background task nor the user expects unconfirmed changes to be considered.
To avoid this scenario, performing edits on a clone of the object is preferable. If edits are canceled, the clone is just discarded. If edits are confirmed, a memento is taken from the clone and restored into the “good” copy.
Conclusion
The Memento pattern is a classic and simple design pattern that has several applications in the UI layer. One of its main drawbacks is that it requires much boilerplate code to implement, but code generators such as Metalama can help reduce repetitive work to almost zero. As always with classic patterns, Memento should not be applied dogmatically. However, it remains a great source of inspiration in your journey to build reliable and user-friendly applications.
This article was first published on a https://blog.postsharp.net under the title The Memento Design Pattern in C#, Practically With Examples [2024].
Top comments (0)