Introduction
If you have already tried to implement your own Undo/Redo pattern or used a third party library, you've probably noticed that you have an explicit dependency with the Undo/Redo service.
Most of the time, the Undo/Redo service is changing a property of a model. What you generally do is tell this service how to modify or revert this property. But it might happen that you forget to use the service to modify the model, modifying the model directly, which might lead to annoying situations (or bugs).
To avoid forgetting the use of the Undo/Redo service, I built a small standalone framework capable of analyzing and recording model changes.
As there is no magic, you still have to implement and respect some rules to make it work, but I tried to concentrate on a system that requires as little code as possible.
Using the Code
Step 1
Create your model and make it implement INotifyPropertyChanged
or inherit NotifyPropertyChangedImpl
.
Mark each property with attribute [IsRecordable
] if you want the property changes to be recorded.
using System.Collections.ObjectModel;
using ElMariachi.WPF.Tools.Modelling;
using ElMariachi.WPF.Tools.Modelling.ModelRecording.Attributes;
namespace MyApplication.Models
{
public class Person : NotifyPropertyChangedImpl
{
private int _age;
private string _name;
private ObservableCollection<Person> _children;
[IsRecordable("Person's name")]
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChanged("Name");
}
}
[IsRecordable("Person's age")]
public int Age
{
get { return _age; }
set
{
_age = value;
NotifyPropertyChanged("Age");
}
}
[IsRecordable("Person's children")]
public ObservableCollection<Person> Children
{
get { return _children; }
set
{
_children = value;
NotifyPropertyChanged("Children");
}
}
}
}
Step 2
Create a new model recorder and give it the model to record.
using ElMariachi.WPF.Tools.Modelling.ModelRecording;
using ElMariachi.WPF.Tools.UndoRedo;
using MyApplication.Models;
namespace MyApplication
{
class App
{
static int Main(string[] args)
{
var person = new Person
{
Age = 39,
Name = "Emiliano",
};
var modelRecorder = new ModelRecorder();
var undoRedoService = new UndoRedoService();
modelRecorder.Record(undoRedoService, person);
person.Age++;
person.Name = "Emiliano El Mariachi";
person.Children.Add(new Person());
undoRedoService.Undo(); undoRedoService.Undo(); undoRedoService.Undo();
return 0;
}
}
}
Advanced Features
Delayed Recording
What is this? You might have noticed that in text editors, when you write some text and then undo (Ctrl+Z), there is a bit of magic which makes that long written text parts are undone instead of each typed text letter.
Sometime, this behaviour makes me think the computer is reading in my mind :-).
Ok, let's dive into more details... The magic of this feature is based on the principle of a triggerable monoflop (for those who are familiar with electronic). The system is based on a delay specifying the time to wait a stable state of the property.
Let's take a small example, with a delay of 1 second. While you type in the editor faster than 1 second, no text change is recorded, but if the text does not changes after 1 second, then a new text record is done with all the changes that occurred between the first typed letter to the elapsed second.
The Model
using System.Collections.ObjectModel;
using ElMariachi.WPF.Tools.Modelling;
using ElMariachi.WPF.Tools.Modelling.ModelRecording.Attributes;
namespace MyApplication.Models
{
public class PersonWithDescription : Person
private string _description;
[IsRecordableWithFilter("Person's description", 1000)]
public string Description
{
get { return _description; }
set
{
_description = value;
NotifyPropertyChanged("Description");
}
}
}
}
Effect
using System.Threading
using ElMariachi.WPF.Tools.Modelling.ModelRecording;
using ElMariachi.WPF.Tools.UndoRedo;
using MyApplication.Models;
namespace MyApplication
{
class App
{
static int Main(string[] args)
{
var person = new PersonWithDescription();
person.Description = "Married";
var modelRecorder = new ModelRecorder();
var undoRedoService = new UndoRedoService();
modelRecorder.Record(undoRedoService, person);
person.Description = "Married with 0";
person.Description = "Married with ";
person.Description = "Married with 1 children";
Thread.Sleep(1500);
undoRedoService.Undo();
return 0;
}
}
}
Features to Come
- Record groups (I'll explain in more detail later)
Try it!
Feel free to download, try and/or modify my library from here:
History
I'll try to keep README.md up-to-date at the root of the repository.
Note: The library also contains an "Automatic Dirty Model Detection", I'll create another article to explain this feature in detail later.