The Memento Design Pattern in C#, Practically With Examples [2024]

by Metalama Team on 25 Jul 2024

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:

  1. 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.

  2. 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.

  3. 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:

Initial App

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();

{}

Subscribe to the Timeless .NET Engineer newsletter
and get access to the complete source code

This article is part of the Timeless .NET Engineer blog. Subscribe to get access to the source code of this article and learn the art of building robust and maintainable applications with less code — work you will be proud of even ten years from now.

Access to the complete source code

    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 allows IMementoCaretaker to retrieve memento objects and restore the state of the object.

      public interface IMementoable
      {
        IMemento SaveToMemento();
      
        void RestoreMemento( IMemento memento );
      }
      
    • The IMemento interface is minimalistic: it just exposes an Originator property used by the IMementoCaretaker during the Undo process.

      public interface IMemento
      {
        IMementoable Originator { get; }
      }
      
    • The IMementoCaretaker is a service that manages stored IMemento instances and uses them to restore states of IMementoable objects.

      public interface IMementoCaretaker
      {
        bool CanUndo { get; }
      
        void CaptureSnapshot( IMementoable mementoable );
      
        void Undo();
      }
      

    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 the Originator property.
      • A constructor that stores the fields and properties of the originator class into the properties of the Memento class, as well as the Originator property.
    • An implementation of the IMementoable interface including:
      • The SaveToMemento method, returning an instance of the Memento nested class.
      • The RestoreMemento method, copying the memento properties back into the originator’s fields and properties.

    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.

    1. Modify the IMemento interface to this:

      public interface IMemento
      {
       IMementoable Originator { get; }
      
       DateTime MementoTime { get; }
      }
      
    2. Modify the implementation pattern to set the value of the MementoTime property to DateTime.Now.
    3. Change Caretaker.CaptureMemento to ignore requests that are too close to each other.

      public void CaptureMemento( IMementoable mementoable )
      {
       if ( this._mementos.Count > 0 )
       {
           var lastMemento = this._mementos.Peek();
      
           if ( lastMemento.MementoTime > DateTime.Now.AddSeconds( -5 )
                && lastMemento.Originator == mementoable )
           {
               // Ignore.
               return;
           }
       }
      
       this._mementos.Push( mementoable.SaveToMemento() );
       this.OnPropertyChanged( nameof(this.CanUndo) );
      }
      

    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.

    Discover Metalama, the leading code generation and validation toolkit for C#

    • Write and maintain less code by eliminating boilerplate, generating it dynamically during compilation, typically reducing code lines and bugs by 15%.
    • Validate your codebase against your own rules in real-time to enforce adherence to your architecture, patterns, and conventions. No need to wait for code reviews.
    • Excel with large, complex, or old codebases. Metalama does not require you to change your architecture. Beyond getting started, it's at scale that it really shines.

    Discover Metalama Free Edition