Memento Design Pattern for Undo/Redo Implementation

I'm working on a pixel art program. One of the goals of this program is about undo and redo operations. This implementation is possible with memento design pattern. Let's mention about memento design pattern at first.

In my opinion, this pattern is the way of storing states of objects. For instance, there is a object we created, this object contains information about location and time. These datas can be changed with some updates. But the old datas should be kept in somewhere. So, if we need to old data, then we have to make undo operation. We could make this with memento design pattern.

Warning - I recommed Command Design Pattern instead of this pattern definitely. Especially, if you work with big size data like images, etc. Because Memento can cause of high memory usage most of time.

I use C#, but normally there is an interface called IEditableObject instead of implementing memento design pattern. But I will not use it. You can find information with a code example from this document.

I drew UML diagram how to use it:

According to the UML, let's start typing the implementation. As I mentioned at the beginning of the article, I will continue to code through the program I am working on now. The part that it will be responsible for in the application will be undo/redo operations. We can say that we undo the change we made on the image.

The class I want to store is the Frame class. Firstly, I created a class called FrameMemento:
using System;
using System.Collections.Generic;

namespace IsometricProgram
{
   
    class FrameMemento
    {  
        private Frame frame;

        public Frame GetFrameMemento { get { return frame; }}

        public FrameMemento(Frame frame)
        {
            this.frame = new Frame(frame);
        }
    }
}
I will use copy constructor for copying the frame object There are another ways to make it. But it seems more safety to me to use it. Also we could use it ICloneable interface or BinaryFormatter for it, but Microsoft does not recommend these ways.

We will create another class called originator. But it's actually current frame itself. I already made this class as Frame. Of course we need to define some methods for memento usage:
using System;
using System.Collections.Generic;

namespace IsometricProgram
{
   
    class Frame
    {  
        ...

        public Frame()
        {
            ...
        }
        
        ...
        
        public FrameMemento CreateFrameMemento()
        {
            return new FrameMemento(this);
        }

        public Frame GetFrameFromMemento(FrameMemento frameMemento)
        {
            return frameMemento.GetFrameMemento;
        }
    }
}
After that, we need to a place to manage these mementos. Therefore, we will use caretaker class. It seems like memento manager to me.
using System;
using System.Collections.Generic;

namespace IsometricProgram
{
   
    class FrameCaretaker
    {  
        private List<FrameMemento> frameMementos;
        
        public int FrameCount { get { return frameMementos.Count; } } 

        public int CurrentFrame { get; set; }

        public FrameCaretaker()
        {
            frameMementos = new List<FrameMemento>();
        }

        public void SetMemento(FrameMemento frameMemento)
        {
            frameMementos.Add(frameMemento);
        }

        public FrameMemento GetMemento(int mementoIndex)
        {
            return frameMementos[mementoIndex];
        }
    }
}
Okay, now we need to use these classes properly. From this point on, the code I typed gets a little more subjective. There is a manager class for frames that I managed frames. Now, I want manage their mementos at this class. So I will create new list that contains FrameCaretaker:
namespace IsometricProgram
{
    class FrameManager
    {
        ...
        
        internal List<Frame> frameList { get; set; }
        internal List<FrameCaretaker> frameCaretakers { get; set; }
        
        ...


        public FrameManager(App app, ..)
        {
            ...

            frameList = new List<Frame>();
            frameCaretakers = new List<FrameCaretaker>();
            
            ...
Namely, when we create a frame, we also create framecaretaker at the same time:
            if(create_button.Rect.Contains(app.MousePos.X, app.MousePos.Y) && app.LeftClick)
            {
            	// created frame in here
                ...
                
                
            	// created framecaretaker and its first memento is added to the list of mementos.
                frameManager.frameCaretakers.Add(new FrameCaretaker());
                // the first memento is created version
                frameManager.frameCaretakers[frameManager.frameCaretakers.Count-1].SetMemento
                (
                    new FrameMemento(frameManager.frameList[frameManager.frameList.Count-1])
                );
                frameManager.frameCaretakers[frameManager.frameCaretakers.Count-1].CurrentFrame = 0;
            }
Now, lets make undo operation and redo operations at first:
        private void UndoOperation()
        {
            if(app.LastEvent == "Undo" && frameCaretakers[app.ActiveFrame].CurrentFrame > 0)
            {
                frameCaretakers[app.ActiveFrame].CurrentFrame--;
                int index = frameCaretakers[app.ActiveFrame].CurrentFrame;

                frameList[app.ActiveFrame] = frameList[app.ActiveFrame].GetFrameFromMemento(
                    frameCaretakers[app.ActiveFrame].GetMemento(index)
                );

                app.LastEvent = "";
            }
        }

        private void RedoOperation()
        {
            if(app.LastEvent == "Redo" && (frameCaretakers[app.ActiveFrame].FrameCount - 1) > frameCaretakers[app.ActiveFrame].CurrentFrame)
            {
                frameCaretakers[app.ActiveFrame].CurrentFrame++;
                int index = frameCaretakers[app.ActiveFrame].CurrentFrame;

                frameList[app.ActiveFrame] = frameList[app.ActiveFrame].GetFrameFromMemento(
                    frameCaretakers[app.ActiveFrame].GetMemento(index)
                );

                app.LastEvent = "";
            }
        }
But, they are not useful for now, because we have just added just one memento at first for each created frame. For example, my drawing operation is done then new momento should be created end of the drawing operation. This is same code that I wrote above in creating frame code block. There is just one difference, we need to increase value of currentframe additionally.

No comments:

Post a Comment