├── .github └── FUNDING.yml ├── src ├── Memento │ ├── Memento.snk │ ├── MementorChangedEventArgs.cs │ ├── MementorChanged.cs │ ├── Events │ │ ├── BaseEvent.cs │ │ ├── ElementAdditionEvent.cs │ │ ├── ElementRemovalEvent.cs │ │ ├── ElementIndexChangeEvent.cs │ │ ├── BatchEvent.cs │ │ └── PropertyChangeEvent.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Memento.csproj │ ├── MementorExtensions.cs │ └── Mementor.cs ├── Memento.Test │ ├── Stubs │ │ ├── InvalidCustomEvent.cs │ │ ├── CustomEvent.cs │ │ ├── Circle.cs │ │ ├── Screen.cs │ │ └── Point.cs │ ├── Session.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Memento.Test.csproj │ └── Features.cs └── Memento.sln ├── .gitignore ├── TODO.md ├── samples └── Plain │ ├── Models.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Plain.csproj │ └── Program.cs └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ['buunguyen'] 2 | -------------------------------------------------------------------------------- /src/Memento/Memento.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buunguyen/memento/HEAD/src/Memento/Memento.snk -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MSBuild 2 | bin 3 | obj 4 | 5 | # MSTest 6 | TestResults 7 | 8 | # ReSharper 9 | _ReSharper.* 10 | 11 | # Users 12 | *.user 13 | *.suo 14 | 15 | # NuGet 16 | *.nupkg -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | These are nice to have: 2 | 3 | * Automatic event reporting, e.g. integrate with [Notify](http://buunguyen.github.com/notify/) 4 | * More samples, custom events... 5 | 6 | Contribution is welcome! -------------------------------------------------------------------------------- /src/Memento/MementorChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | /// 4 | /// Represents argument of the event. 5 | /// 6 | public class MementorChangedEventArgs 7 | { 8 | /// 9 | /// The event associated with the the event. 10 | /// 11 | public BaseEvent Event { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Memento.Test/Stubs/InvalidCustomEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Memento.Test.Stubs 2 | { 3 | internal class InvalidCustomEvent : BaseEvent 4 | { 5 | public BatchEvent Batch { get; set; } 6 | 7 | public InvalidCustomEvent(BatchEvent batch) 8 | { 9 | Batch = batch; 10 | } 11 | 12 | protected override BaseEvent Rollback() 13 | { 14 | return Batch; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Memento.Test/Stubs/CustomEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Memento.Test.Stubs 2 | { 3 | internal class CustomEvent : BaseEvent 4 | { 5 | public bool IsRolledback { get; set; } 6 | public CustomEvent ReverseEvent { get; set; } 7 | 8 | public CustomEvent(CustomEvent reverseEvent) 9 | { 10 | ReverseEvent = reverseEvent; 11 | } 12 | 13 | protected override BaseEvent Rollback() 14 | { 15 | IsRolledback = true; 16 | return ReverseEvent; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Memento.Test/Session.cs: -------------------------------------------------------------------------------- 1 | namespace Memento.Test 2 | { 3 | /// 4 | /// The same mementor instance is usually used throughout the application 5 | /// or editor session, therefore there should be some centralized way to 6 | /// get that instance. 7 | /// 8 | internal static class Session 9 | { 10 | public static Mementor Mementor; 11 | 12 | public static Mementor New() 13 | { 14 | return Mementor = new Mementor(); 15 | } 16 | 17 | public static void End() 18 | { 19 | Mementor.Dispose(); 20 | Mementor = null; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Memento/MementorChanged.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | /// 4 | /// Fired whenever there is a change in either the undo or redo stack of a instance. 5 | /// Spefically, these actions will trigger this event: 6 | /// 7 | /// Any change marked by client code 8 | /// 's: , and possibly unless the undo and redo stacks are already empty. 9 | /// 10 | /// 11 | /// The firing mementor object. 12 | /// The event argument. 13 | public delegate void MementorChanged(Mementor sender, MementorChangedEventArgs args); 14 | } -------------------------------------------------------------------------------- /src/Memento.Test/Stubs/Circle.cs: -------------------------------------------------------------------------------- 1 | namespace Memento.Test.Stubs 2 | { 3 | internal class Circle 4 | { 5 | private Point _center; 6 | 7 | public Point Center 8 | { 9 | get { return _center; } 10 | set 11 | { 12 | if (_center != value) { 13 | Session.Mementor.PropertyChange(this, () => Center); 14 | _center = value; 15 | } 16 | } 17 | } 18 | 19 | private int _radius; 20 | 21 | public int Radius 22 | { 23 | get { return _radius; } 24 | set 25 | { 26 | if (_radius != value) { 27 | Session.Mementor.PropertyChange(this, () => Radius); 28 | _radius = value; 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Memento/Events/BaseEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | using System; 4 | 5 | /// 6 | /// Must be implemented by all events. 7 | /// 8 | public abstract class BaseEvent 9 | { 10 | /// 11 | /// Rollback this event. This method is executed with 12 | /// off, so no change marking will be done during its execution. 13 | /// Because undo and redo are symmetric, this method might return a 14 | /// "reverse event" which will be used to rollback the effect of the current method. 15 | /// This method must now, however, return an isntance of . 16 | /// 17 | /// A symmetric reverse event for this rollback action. 18 | protected internal abstract BaseEvent Rollback(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Memento.Test/Stubs/Screen.cs: -------------------------------------------------------------------------------- 1 | namespace Memento.Test.Stubs 2 | { 3 | using System.Collections.Generic; 4 | 5 | internal class Screen 6 | { 7 | public Screen() 8 | { 9 | Shapes = new List(); 10 | } 11 | 12 | public IList Shapes { get; set; } 13 | 14 | public void Add(Circle circle) 15 | { 16 | Session.Mementor.ElementAdd(Shapes, circle); 17 | Shapes.Add(circle); 18 | } 19 | 20 | public void Remove(Circle circle) 21 | { 22 | Session.Mementor.ElementRemove(Shapes, circle); 23 | Shapes.Remove(circle); 24 | } 25 | 26 | public void MoveToFront(int index) 27 | { 28 | Circle circle = Shapes[index]; 29 | Session.Mementor.ElementIndexChange(Shapes, circle); 30 | Shapes.Remove(circle); 31 | Shapes.Insert(0, circle); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /samples/Plain/Models.cs: -------------------------------------------------------------------------------- 1 | namespace Plain 2 | { 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | 6 | internal class Class : ObservableCollection 7 | { 8 | } 9 | 10 | internal class Student : INotifyPropertyChanging 11 | { 12 | private string _name; 13 | 14 | public string Name 15 | { 16 | get { return _name; } 17 | set 18 | { 19 | if (_name != value) { 20 | OnPropertyChanging("Name"); 21 | _name = value; 22 | } 23 | } 24 | } 25 | 26 | public event PropertyChangingEventHandler PropertyChanging; 27 | 28 | private void OnPropertyChanging(string propertyName) 29 | { 30 | if (PropertyChanging != null) 31 | PropertyChanging(this, new PropertyChangingEventArgs(propertyName)); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Memento.Test/Stubs/Point.cs: -------------------------------------------------------------------------------- 1 | namespace Memento.Test.Stubs 2 | { 3 | internal struct Point 4 | { 5 | public readonly int X; 6 | public readonly int Y; 7 | 8 | public Point(int x, int y) 9 | { 10 | X = x; 11 | Y = y; 12 | } 13 | 14 | public bool Equals(Point other) 15 | { 16 | return other.Y == Y && other.X == X; 17 | } 18 | 19 | public override bool Equals(object obj) 20 | { 21 | return obj is Point && Equals((Point) obj); 22 | } 23 | 24 | public override int GetHashCode() 25 | { 26 | return unchecked(Y*397 ^ X); 27 | } 28 | 29 | public static bool operator ==(Point left, Point right) 30 | { 31 | return Equals(left, right); 32 | } 33 | 34 | public static bool operator !=(Point left, Point right) 35 | { 36 | return !Equals(left, right); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Memento/Events/ElementAdditionEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | /// 7 | /// Represents a collection element addition event. 8 | /// 9 | public sealed class ElementAdditionEvent : BaseEvent 10 | { 11 | /// 12 | /// The collection this event occurs on. 13 | /// 14 | public IList Collection { get; private set; } 15 | 16 | /// 17 | /// The element added. 18 | /// 19 | public T Element { get; private set; } 20 | 21 | /// 22 | /// Creates the event. 23 | /// 24 | /// The collection object. 25 | /// The element added. 26 | public ElementAdditionEvent(IList collection, T element) 27 | { 28 | if (collection == null) throw new ArgumentNullException("collection"); 29 | Collection = collection; 30 | Element = element; 31 | } 32 | 33 | protected internal override BaseEvent Rollback() 34 | { 35 | var reverse = new ElementRemovalEvent(Collection, Element); 36 | Collection.Remove(Element); 37 | return reverse; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Memento.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Memento.Test")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Memento.Test")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("04fcab1a-0f37-4033-933d-090ce45e7e69")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /samples/Plain/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Plain")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Plain")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("de4d54c6-d969-490d-bb38-8218f0723cc4")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Memento/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. AtomicEvent these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Memento")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Memento")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("bdf67f8f-0aad-4ec8-8abf-12dbeb3cc100")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Memento/Events/ElementRemovalEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | /// 7 | /// Represents a collection element removal event. 8 | /// 9 | public sealed class ElementRemovalEvent : BaseEvent 10 | { 11 | /// 12 | /// The collection this event occurs on. 13 | /// 14 | public IList Collection { get; private set; } 15 | 16 | /// 17 | /// The element removed. 18 | /// 19 | public T Element { get; private set; } 20 | 21 | /// 22 | /// The index to be restored too when undo. 23 | /// 24 | public int Index { get; private set; } 25 | 26 | /// 27 | /// Creates the event. 28 | /// 29 | /// The collection object. 30 | /// The element to be removed. 31 | /// The index of the element in the collection. If not supplied, use current index of in the . 32 | public ElementRemovalEvent(IList collection, T element, int? index = null) 33 | { 34 | if (collection == null) throw new ArgumentNullException("collection"); 35 | index = index ?? collection.IndexOf(element); 36 | if (index == -1) 37 | throw new ArgumentException("Must provide a valid index if element does not exist in the collection"); 38 | 39 | Collection = collection; 40 | Element = element; 41 | Index = index.Value; 42 | } 43 | 44 | protected internal override BaseEvent Rollback() 45 | { 46 | var reverse = new ElementAdditionEvent(Collection, Element); 47 | Collection.Insert(Index, Element); 48 | return reverse; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/Memento/Events/ElementIndexChangeEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | /// 7 | /// Represents a collection element index change event. 8 | /// 9 | public sealed class ElementIndexChangeEvent : BaseEvent 10 | { 11 | /// 12 | /// The collection this event occurs on. 13 | /// 14 | public IList Collection { get; private set; } 15 | 16 | /// 17 | /// The element whose index was changed. 18 | /// 19 | public T Element { get; private set; } 20 | 21 | /// 22 | /// The index to be restored too when undo. 23 | /// 24 | public int Index { get; private set; } 25 | 26 | /// 27 | /// Creates the event. 28 | /// 29 | /// The collection object. 30 | /// The element whose index is being changed. 31 | /// The index of the element in the collection. If not supplied, use current index of in the . 32 | public ElementIndexChangeEvent(IList collection, T element, int? index = null) 33 | { 34 | if (collection == null) throw new ArgumentNullException("collection"); 35 | index = index ?? collection.IndexOf(element); 36 | if (index == -1) 37 | throw new ArgumentException("Must provide a valid index if element does not exist in the collection"); 38 | Collection = collection; 39 | Element = element; 40 | Index = index.Value; 41 | } 42 | 43 | protected internal override BaseEvent Rollback() 44 | { 45 | var reverse = new ElementIndexChangeEvent(Collection, Element); 46 | Collection.Remove(Element); 47 | Collection.Insert(Index, Element); 48 | return reverse; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/Memento/Events/BatchEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | /// 9 | /// Represents a batch of events. 10 | /// 11 | public sealed class BatchEvent : BaseEvent, IEnumerable 12 | { 13 | private readonly Stack _events; 14 | 15 | internal BatchEvent(BatchEvent other = null) 16 | { 17 | _events = other == null 18 | ? new Stack() 19 | : new Stack(other._events.Reverse()); 20 | } 21 | 22 | /// 23 | /// The number of events in this batch. 24 | /// 25 | public int Count 26 | { 27 | get { return _events.Count; } 28 | } 29 | 30 | internal BaseEvent Pop() 31 | { 32 | return _events.Pop(); 33 | } 34 | 35 | internal void Push(BaseEvent @event) 36 | { 37 | _events.Push(@event); 38 | } 39 | 40 | internal void Clear() 41 | { 42 | _events.Clear(); 43 | } 44 | 45 | protected internal override BaseEvent Rollback() 46 | { 47 | var batch = new BatchEvent(); 48 | while (Count > 0) { 49 | var reverse = Pop().Rollback(); 50 | if (reverse == null) continue; 51 | if (reverse is BatchEvent) throw new InvalidOperationException("Must not return BatchEvent in Rollback()"); 52 | batch.Push(reverse); 53 | } 54 | return batch; 55 | } 56 | 57 | /// 58 | /// Returns the enumerator to access child events. 59 | /// 60 | /// The enumerator to access child events. 61 | public IEnumerator GetEnumerator() 62 | { 63 | return _events.GetEnumerator(); 64 | } 65 | 66 | IEnumerator IEnumerable.GetEnumerator() 67 | { 68 | return GetEnumerator(); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/Memento/Events/PropertyChangeEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | 7 | /// 8 | /// Represents a property value change event. 9 | /// 10 | public sealed class PropertyChangeEvent : BaseEvent 11 | { 12 | /// 13 | /// The target object this event occurs on. 14 | /// 15 | public object TargetObject { get; private set; } 16 | 17 | /// 18 | /// The name of the changed property. 19 | /// 20 | public string PropertyName { get; private set; } 21 | 22 | /// 23 | /// The value to be restored to when undo. 24 | /// 25 | public object PropertyValue { get; private set; } 26 | 27 | /// 28 | /// Creates the event. 29 | /// 30 | /// The target object whose property is changed. 31 | /// The name of the property being changed. 32 | /// The value of the property. If not supplied, use the current value of in 33 | public PropertyChangeEvent(object target, string propertyName, object propertyValue = null) 34 | { 35 | if (target == null) throw new ArgumentNullException("target"); 36 | if (propertyName == null) throw new ArgumentNullException("propertyName"); 37 | TargetObject = target; 38 | PropertyName = propertyName; 39 | PropertyValue = propertyValue ?? PropertyInfo().GetValue(target, null); 40 | } 41 | 42 | protected internal override BaseEvent Rollback() 43 | { 44 | var reverse = new PropertyChangeEvent(TargetObject, PropertyName); 45 | PropertyInfo().SetValue(TargetObject, PropertyValue, null); 46 | return reverse; 47 | } 48 | 49 | private PropertyInfo PropertyInfo() 50 | { 51 | return TargetObject.GetType().GetProperty(PropertyName, 52 | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /samples/Plain/Plain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258} 9 | Exe 10 | Properties 11 | Plain 12 | Plain 13 | v4.0 14 | Client 15 | 512 16 | 17 | 18 | x86 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | x86 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805} 53 | Memento 54 | 55 | 56 | 57 | 64 | -------------------------------------------------------------------------------- /src/Memento.Test/Memento.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | 7 | 8 | 2.0 9 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E} 10 | Library 11 | Properties 12 | Memento.Test 13 | Memento.Test 14 | v4.0 15 | 512 16 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 3.5 40 | 41 | 42 | 43 | 44 | False 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805} 60 | Memento 61 | 62 | 63 | 64 | 71 | -------------------------------------------------------------------------------- /src/Memento/Memento.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805} 9 | Library 10 | Properties 11 | Memento 12 | Memento 13 | v4.0 14 | 512 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | bin\Debug\Memento.xml 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | bin\Release\Memento.xml 34 | 35 | 36 | true 37 | 38 | 39 | Memento.snk 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 74 | -------------------------------------------------------------------------------- /samples/Plain/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Plain 2 | { 3 | using System; 4 | using System.Collections.ObjectModel; 5 | using System.Collections.Specialized; 6 | using System.ComponentModel; 7 | using System.Linq; 8 | using Memento; 9 | 10 | /// 11 | /// While Mementor can be used with POCOs (as demonstrated in the unit test suite), 12 | /// this sample program demonstrates how to use Mementor with objects implementing 13 | /// and 14 | /// just via event handling instead of mixing Memento calls into model classes. 15 | /// 16 | internal class Program 17 | { 18 | /// 19 | /// doesn't maintain old index, 20 | /// so use this cache in order to retrieve index whenever marking a removal event. 21 | /// 22 | private static ObservableCollection Cache; 23 | private static readonly Mementor M = new Mementor(); 24 | 25 | private static void Main() 26 | { 27 | var cls = new Class(); 28 | Cache = new ObservableCollection(cls); 29 | cls.CollectionChanged += ClassChanged; 30 | 31 | cls.Add(new Student()); 32 | cls.Add(new Student()); 33 | cls[0].Name = "Peter"; 34 | cls[1].Name = "John"; 35 | cls.Move(0, 1); 36 | cls.RemoveAt(1); 37 | 38 | Dump(cls); 39 | 40 | M.Changed += (_, args) => Console.WriteLine("Undo event: " + args.Event.GetType()); 41 | while (M.CanUndo) { 42 | M.Undo(); 43 | Dump(cls); 44 | } 45 | } 46 | 47 | private static void Dump(Class cls) 48 | { 49 | Console.WriteLine("Class information"); 50 | Console.WriteLine("Count: " + cls.Count); 51 | for (int i = 0; i < cls.Count; i++) { 52 | Console.WriteLine("Student[{0}]: {1}", i+1, cls[i].Name); 53 | } 54 | Console.WriteLine(); 55 | } 56 | 57 | private static void ClassChanged(object sender, NotifyCollectionChangedEventArgs args) 58 | { 59 | switch (args.Action) { 60 | case NotifyCollectionChangedAction.Move: 61 | M.ElementIndexChange((Class) sender, (Student) args.OldItems[0], args.OldStartingIndex); 62 | Cache = new ObservableCollection((Class)sender); 63 | break; 64 | case NotifyCollectionChangedAction.Add: 65 | case NotifyCollectionChangedAction.Replace: 66 | case NotifyCollectionChangedAction.Remove: 67 | if (args.OldItems != null) { 68 | foreach (var student in args.OldItems.Cast()) { 69 | M.ElementRemove((Class)sender, student, Cache.IndexOf(student)); 70 | student.PropertyChanging -= StudentChanging; 71 | } 72 | } 73 | 74 | if (args.NewItems != null) { 75 | foreach (var student in args.NewItems.Cast()) { 76 | M.ElementAdd((Class) sender, student); 77 | student.PropertyChanging += StudentChanging; 78 | } 79 | } 80 | 81 | Cache = new ObservableCollection((Class)sender); 82 | break; 83 | default: 84 | throw new NotSupportedException(); 85 | } 86 | } 87 | 88 | private static void StudentChanging(object sender, PropertyChangingEventArgs args) 89 | { 90 | M.PropertyChange(sender, args.PropertyName); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Memento.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Memento", "Memento\Memento.csproj", "{4A5BE9B5-818A-465F-A2EF-C23AFFC96805}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Memento.Test", "Memento.Test\Memento.Test.csproj", "{10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "misc", "misc", "{D1D42AE0-D149-4370-B85E-6851A094CE59}" 9 | ProjectSection(SolutionItems) = preProject 10 | ..\build\build.bat = ..\build\build.bat 11 | ..\build\Memento.nuspec = ..\build\Memento.nuspec 12 | ..\README.md = ..\README.md 13 | ..\TODO.md = ..\TODO.md 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{3E89811B-E9F0-44D8-9E77-5EEA439EA9B4}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plain", "..\samples\Plain\Plain.csproj", "{F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Debug|Mixed Platforms = Debug|Mixed Platforms 24 | Debug|x86 = Debug|x86 25 | Release|Any CPU = Release|Any CPU 26 | Release|Mixed Platforms = Release|Mixed Platforms 27 | Release|x86 = Release|x86 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 33 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 34 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Debug|x86.ActiveCfg = Debug|Any CPU 35 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 38 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Release|Mixed Platforms.Build.0 = Release|Any CPU 39 | {4A5BE9B5-818A-465F-A2EF-C23AFFC96805}.Release|x86.ActiveCfg = Release|Any CPU 40 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 43 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 44 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Debug|x86.ActiveCfg = Debug|Any CPU 45 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 48 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Release|Mixed Platforms.Build.0 = Release|Any CPU 49 | {10E2CC60-6E55-48D5-B9DD-E76155AFAF8E}.Release|x86.ActiveCfg = Release|Any CPU 50 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Debug|Any CPU.ActiveCfg = Debug|x86 51 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 52 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Debug|Mixed Platforms.Build.0 = Debug|x86 53 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Debug|x86.ActiveCfg = Debug|x86 54 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Debug|x86.Build.0 = Debug|x86 55 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Release|Any CPU.ActiveCfg = Release|x86 56 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Release|Mixed Platforms.ActiveCfg = Release|x86 57 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Release|Mixed Platforms.Build.0 = Release|x86 58 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Release|x86.ActiveCfg = Release|x86 59 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258}.Release|x86.Build.0 = Release|x86 60 | EndGlobalSection 61 | GlobalSection(SolutionProperties) = preSolution 62 | HideSolutionNode = FALSE 63 | EndGlobalSection 64 | GlobalSection(NestedProjects) = preSolution 65 | {F4BD6EBD-04FA-4A7D-9196-3C3761C4B258} = {3E89811B-E9F0-44D8-9E77-5EEA439EA9B4} 66 | EndGlobalSection 67 | EndGlobal 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Shelter](https://www.codeshelter.co/static/badges/badge-flat.svg)](https://www.codeshelter.co/) 2 | 3 | About 4 | ======= 5 | 6 | Memento is a lightweight and extensible undo framework for .NET applications. 7 | It provide undo and redo support for the following events(*) out of the box: 8 | 9 | * Property change 10 | * Collection element addition 11 | * Collection element removal 12 | * Collection element's index change 13 | 14 | While these basic events have proved to be sufficient for most applications I've worked with, 15 | Memento allows you to build custom events and plug into the framework. 16 | 17 | **Note**: *events* here mean those actions that cause changes to the system state, e.g. change 18 | value of a property etc. They are not .NET events. 19 | 20 | Getting Started 21 | ======= 22 | 23 | Install from NuGet 24 | ```csharp 25 | Install-Package Memento 26 | ``` 27 | 28 | Create an instance of a `Mementor`. 29 | ```csharp 30 | var mementor = Mementor.Create(); 31 | ``` 32 | **Note**: it's typically sufficient to use one single instance for an entire application. 33 | But you can create multiple instances as needed and each of them has a different undo/redo 34 | stack. Regardless, make sure you store this instance somewhere easily accessible to other 35 | parts of code. 36 | 37 | ### Mark property changes 38 | ```csharp 39 | // Mark change via expression syntax 40 | mementor.PropertyChange(shape, () => shape.Radius); 41 | 42 | // Mark change via string name 43 | mementor.PropertyChange(shape, "Radius"); 44 | 45 | // Mark change with explicit property value (which will be restored to when undo) 46 | mementor.PropertyChange(shape, "Radius", 10); 47 | ``` 48 | 49 | ### Mark collection changes 50 | ```csharp 51 | // Addition 52 | mementor.ElementAdd(screen, shape); 53 | 54 | // Removal 55 | mementor.ElementRemove(screen, shape); 56 | 57 | // Removal with explicit index (to be restored to when undo) 58 | mementor.ElementRemove(screen, shape, index); 59 | 60 | // Index change 61 | mementor.ElementIndexChange(screen, shape); 62 | 63 | // Index change with explicit index (to be restored to when undo) 64 | mementor.ElementIndexChange(screen, shape, index); 65 | ``` 66 | ### Perform undo and redo 67 | ```csharp 68 | // Undo the last event 69 | if (mementor.CanUndo) mementor.Undo(); 70 | 71 | // Redo a previous undo 72 | if (mementor.CanRedo) mementor.Redo(); 73 | ``` 74 | 75 | ### Reset 76 | 77 | At any point of time, you can reset a `Mementor` to its original state. 78 | ```csharp 79 | mementor.Reset(); 80 | ``` 81 | 82 | ### Batch multiple changes 83 | 84 | If you want to undo multiple events at once, batch them together. 85 | ```csharp 86 | // Batch via block 87 | mementor.Batch(() => { 88 | // change events happen here 89 | }); 90 | 91 | // Undo all events in the previous batch 92 | mementor.Undo(); 93 | 94 | // Batch explicitly 95 | mementor.BeginBatch(); 96 | // ... sometime later 97 | mementor.EndBatch(); 98 | 99 | ``` 100 | 101 | ### Disable change marking 102 | 103 | If you want to temporarily disable marking (effectively making `Mementor` ignores 104 | all calls to change marking methods like `PropertyChange`), do one of the followings. 105 | ```csharp 106 | mementor.ExecuteNoTrack(() => { 107 | // changes happened in this block are ignored 108 | }); 109 | 110 | mementor.IsTrackingEnabled = false; 111 | ``` 112 | 113 | ### Event handling 114 | 115 | You can be notified when there is change to the undo/redo stack of a `Mementor` 116 | by handling its `Changed` event. For example if you call `Undo()`, this event 117 | will be fired with the associated undone event. 118 | ```csharp 119 | mementor.Changed += (_, args) => { 120 | // args allow you to access to the event associated with this notification 121 | } 122 | ``` 123 | 124 | ### Custom events 125 | 126 | You can write your own custom event by extending `Memento.BaseEvent` class. 127 | Then you can use it with a `Mementor` as follows. 128 | ```csharp 129 | mementor.MarkEvent(customEvent); 130 | ``` 131 | 132 | ### Want to learn more? 133 | The comprehensive test suite for Memento should be a good source of reference. 134 | 135 | Contact 136 | ======= 137 | 138 | * Email: [buunguyen@gmail.com](mailto:buunguyen@gmail.com) 139 | * Blog: [www.buunguyen.net](http://www.buunguyen.net/blog) 140 | * Twitter: [@buunguyen](https://twitter.com/buunguyen/) 141 | -------------------------------------------------------------------------------- /src/Memento/MementorExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq.Expressions; 6 | 7 | /// 8 | /// Convenient extension methods to mark built-in events. 9 | /// 10 | public static class MementorExtensions 11 | { 12 | /// 13 | /// Marks a property change event. 14 | /// 15 | /// The type of the property. 16 | /// The mementor object. 17 | /// The target object. 18 | /// The property selector expression. 19 | /// The value to be restored to when undo. 20 | /// If not supplied, will be retrieved directly from the . 21 | public static void PropertyChange(this Mementor mementor, 22 | object target, Expression> propertySelector, object previousValue = null) 23 | { 24 | if (mementor.IsTrackingEnabled) 25 | PropertyChange(mementor, target, propertySelector.Name(), previousValue); 26 | } 27 | 28 | private static string Name(this Expression> propertySelector) 29 | { 30 | return ((MemberExpression)propertySelector.Body).Member.Name; 31 | } 32 | 33 | /// 34 | /// Marks a property change event. 35 | /// 36 | /// The mementor object. 37 | /// The target object. 38 | /// The name of the property. 39 | /// The value to be restored to when undo. 40 | /// If not supplied, will be retrieved directly from the . 41 | public static void PropertyChange(this Mementor mementor, 42 | object target, string propertyName, object previousValue = null) 43 | { 44 | if (mementor.IsTrackingEnabled) 45 | mementor.MarkEvent(new PropertyChangeEvent(target, propertyName, previousValue)); 46 | } 47 | 48 | /// 49 | /// Marks an element addition event. 50 | /// 51 | /// The generic type parameter of the collection. 52 | /// The mementor object. 53 | /// The collection object. 54 | /// The element being added. 55 | public static void ElementAdd(this Mementor mementor, 56 | IList collection, T element) 57 | { 58 | if (mementor.IsTrackingEnabled) 59 | mementor.MarkEvent(new ElementAdditionEvent(collection, element)); 60 | } 61 | 62 | /// 63 | /// Marks an element removal event. 64 | /// 65 | /// The generic type parameter of the collection. 66 | /// The mementor object. 67 | /// The collection object. 68 | /// The element being removed 69 | /// The index of the element being removed. If not supplied, will retrieve via . 70 | public static void ElementRemove(this Mementor mementor, 71 | IList collection, T element, int? elementIndex = null) 72 | { 73 | if (mementor.IsTrackingEnabled) 74 | mementor.MarkEvent(new ElementRemovalEvent(collection, element, elementIndex)); 75 | } 76 | 77 | /// 78 | /// Marks an element index change event. 79 | /// 80 | /// The generic type parameter of the collection. 81 | /// The mementor object. 82 | /// The collection object. 83 | /// The element whose index is being changed 84 | /// The index of the element being removed. If not supplied, will retrieve via . 85 | public static void ElementIndexChange(this Mementor mementor, 86 | IList collection, T element, int? elementIndex = null) 87 | { 88 | if (mementor.IsTrackingEnabled) 89 | mementor.MarkEvent(new ElementIndexChangeEvent(collection, element, elementIndex)); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Memento/Mementor.cs: -------------------------------------------------------------------------------- 1 | namespace Memento 2 | { 3 | using System; 4 | using System.Linq; 5 | 6 | /// 7 | /// Provides undo and redo services. 8 | /// 9 | public class Mementor : IDisposable 10 | { 11 | /// 12 | /// Fired after an undo or redo is performed. 13 | /// 14 | public event MementorChanged Changed; 15 | 16 | private readonly BatchEvent _undoStack = new BatchEvent(); 17 | private readonly BatchEvent _redoStack = new BatchEvent(); 18 | private BatchEvent _currentBatch; 19 | 20 | /// 21 | /// Creates an instance of . 22 | /// 23 | /// Whether this instance is enabled or not. 24 | public Mementor(bool isEnabled = true) 25 | { 26 | IsTrackingEnabled = isEnabled; 27 | } 28 | 29 | #region Core 30 | 31 | /// 32 | /// Marks an event. This method also serves as an extensibility point for custom events. 33 | /// 34 | /// The event to be marked. 35 | public void MarkEvent(BaseEvent anEvent) 36 | { 37 | if (!IsTrackingEnabled) return; 38 | if (anEvent == null) throw new ArgumentNullException("anEvent"); 39 | (_currentBatch ?? _undoStack).Push(anEvent); 40 | if (!IsInBatch) PerformPostMarkAction(anEvent); 41 | } 42 | 43 | /// 44 | /// Marks a batch during which all events are combined so that only needs calling once. 45 | /// 46 | /// The code block performing batch change marking. 47 | /// 48 | /// Batches cannot be nested. At any point, there must be only one active batch. 49 | public void Batch(Action codeBlock) 50 | { 51 | if (!IsTrackingEnabled) return; 52 | if (codeBlock == null) throw new ArgumentNullException("codeBlock"); 53 | 54 | BeginBatch(); 55 | try { 56 | codeBlock(); 57 | } 58 | finally { 59 | // Must not call EndBatch() because CheckPreconditions() might return false 60 | BaseEvent @event = InternalEndBatch(_undoStack); 61 | if (@event != null) 62 | PerformPostMarkAction(@event); 63 | } 64 | } 65 | 66 | /// 67 | /// Explicitly marks the beginning of a batch. Use this instead of 68 | /// changes can be made in different places instead of inside one certain block of code. 69 | /// When finish, end the batch by invoking . 70 | /// 71 | public void BeginBatch() 72 | { 73 | if (!IsTrackingEnabled) return; 74 | if (IsInBatch) throw new InvalidOperationException("Re-entrant batch is not supported"); 75 | 76 | _currentBatch = new BatchEvent(); 77 | } 78 | 79 | /// 80 | /// Ends a batch. 81 | /// 82 | public void EndBatch() 83 | { 84 | if (!IsTrackingEnabled) return; 85 | if (!IsInBatch) throw new InvalidOperationException("A batch has not been started yet"); 86 | 87 | BaseEvent @event = InternalEndBatch(_undoStack); 88 | if (@event != null) 89 | PerformPostMarkAction(@event); 90 | } 91 | 92 | /// 93 | /// Executes the supplied code block with turned off. 94 | /// 95 | /// The code block to be executed. 96 | /// 97 | public void ExecuteNoTrack(Action codeBlock) 98 | { 99 | var previousState = IsTrackingEnabled; 100 | IsTrackingEnabled = false; 101 | try { 102 | codeBlock(); 103 | } 104 | finally { 105 | IsTrackingEnabled = previousState; 106 | } 107 | } 108 | 109 | /// 110 | /// Performs an undo. 111 | /// 112 | public void Undo() 113 | { 114 | if (!CanUndo) throw new InvalidOperationException("There is nothing to undo"); 115 | if (IsInBatch) throw new InvalidOperationException("Finish the active batch first"); 116 | 117 | var @event = _undoStack.Pop(); 118 | RollbackEvent(@event is BatchEvent ? new BatchEvent((BatchEvent) @event) : @event, true); 119 | NotifyChange(@event); 120 | } 121 | 122 | /// 123 | /// Performs a redo. 124 | /// 125 | public void Redo() 126 | { 127 | if (!CanRedo) throw new InvalidOperationException("There is nothing to redo"); 128 | if (IsInBatch) throw new InvalidOperationException("Finish the active batch first"); 129 | 130 | var @event = _redoStack.Pop(); 131 | RollbackEvent(@event is BatchEvent ? new BatchEvent((BatchEvent) @event) : @event, false); 132 | NotifyChange(@event); 133 | } 134 | 135 | /// 136 | /// Returns true if can undo. 137 | /// 138 | public bool CanUndo 139 | { 140 | get { return _undoStack.Count > 0; } 141 | } 142 | 143 | /// 144 | /// Returns true if can redo. 145 | /// 146 | public bool CanRedo 147 | { 148 | get { return _redoStack.Count > 0; } 149 | } 150 | 151 | /// 152 | /// How many undos in the stack. 153 | /// 154 | public int UndoCount 155 | { 156 | get { return _undoStack.Count; } 157 | } 158 | 159 | /// 160 | /// How many redos in the stack. 161 | /// 162 | public int RedoCount 163 | { 164 | get { return _redoStack.Count; } 165 | } 166 | 167 | /// 168 | /// If true, all calls to mark changes are ignored. 169 | /// 170 | /// 171 | public bool IsTrackingEnabled { get; set; } 172 | 173 | /// 174 | /// Returns true if a batch is being already begun but not ended. 175 | /// 176 | public bool IsInBatch 177 | { 178 | get { return _currentBatch != null; } 179 | } 180 | 181 | /// 182 | /// Resets the state of this object to its initial state. 183 | /// This effectively clears the redo stack, undo stack and current batch (if one is active). 184 | /// 185 | public void Reset() 186 | { 187 | bool shouldNotify = UndoCount > 0 || RedoCount > 0; 188 | _undoStack.Clear(); 189 | _redoStack.Clear(); 190 | _currentBatch = null; 191 | IsTrackingEnabled = true; 192 | if (shouldNotify) NotifyChange(null); 193 | } 194 | 195 | /// 196 | /// Disposes the this mementor and clears redo and undo stacks. 197 | /// This method won't fire event. 198 | /// 199 | public void Dispose() 200 | { 201 | Changed = null; 202 | _undoStack.Clear(); 203 | _redoStack.Clear(); 204 | _currentBatch = null; 205 | } 206 | 207 | #endregion 208 | 209 | #region Private 210 | 211 | private void RollbackEvent(BaseEvent @event, bool undoing) 212 | { 213 | ExecuteNoTrack(() => { 214 | var reverse = @event.Rollback(); 215 | if (reverse == null) return; 216 | if (reverse is BatchEvent) 217 | { 218 | if (!(@event is BatchEvent)) 219 | throw new InvalidOperationException("Must not return BatchEvent in Rollback()"); 220 | reverse = ProcessBatch((BatchEvent) reverse); 221 | if (reverse == null) return; 222 | } 223 | (undoing ? _redoStack : _undoStack).Push(reverse); 224 | }); 225 | } 226 | 227 | private BaseEvent InternalEndBatch(BatchEvent stack) 228 | { 229 | BaseEvent processed = ProcessBatch(_currentBatch); 230 | if (processed != null) stack.Push(processed); 231 | _currentBatch = null; 232 | return processed; 233 | } 234 | 235 | private BaseEvent ProcessBatch(BatchEvent batchEvent) 236 | { 237 | if (batchEvent.Count == 0) return null; 238 | if (batchEvent.Count == 1) return batchEvent.Pop(); 239 | return batchEvent; 240 | } 241 | 242 | private void PerformPostMarkAction(BaseEvent @event) 243 | { 244 | _redoStack.Clear(); 245 | NotifyChange(@event); 246 | } 247 | 248 | private void NotifyChange(BaseEvent @event) 249 | { 250 | if (Changed != null) Changed(this, new MementorChangedEventArgs {Event = @event}); 251 | } 252 | 253 | #endregion 254 | } 255 | } -------------------------------------------------------------------------------- /src/Memento.Test/Features.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | 3 | namespace Memento.Test 4 | { 5 | using System; 6 | using System.Linq; 7 | using Memento.Test.Stubs; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | 10 | [TestClass] 11 | public class Features 12 | { 13 | private Mementor m; 14 | 15 | [TestInitialize] 16 | public void Setup() 17 | { 18 | m = Session.New(); 19 | } 20 | 21 | [TestCleanup] 22 | public void Cleanup() 23 | { 24 | Session.End(); 25 | } 26 | 27 | [TestMethod] 28 | public void Should_initialize_correctly() 29 | { 30 | UndoCount(0).RedoCount(0); 31 | Assert.IsTrue(m.IsTrackingEnabled); 32 | } 33 | 34 | [TestMethod] 35 | public void Should_undo_redo_property_change() 36 | { 37 | var c = new Circle(); 38 | for (int i = 0; i < 10; i++) { 39 | c.Radius = i + 1; 40 | UndoCount(i + 1).RedoCount(0); 41 | } 42 | for (int i = 9; i >= 0; i--) { 43 | m.Undo(); 44 | Assert.AreEqual(i, c.Radius); 45 | UndoCount(i).RedoCount(9 - i + 1); 46 | } 47 | for (int i = 0; i < 10; i++) { 48 | m.Redo(); 49 | Assert.AreEqual(i + 1, c.Radius); 50 | UndoCount(i + 1).RedoCount(9 - i); 51 | } 52 | } 53 | 54 | [TestMethod] 55 | public void Should_allow_provide_property_value() 56 | { 57 | var c = new Circle(); 58 | UndoCount(0); 59 | m.PropertyChange(c, () => c.Radius, 10); 60 | m.Undo(); 61 | Assert.AreEqual(10, c.Radius); 62 | } 63 | 64 | [TestMethod] 65 | public void Should_undo_redo_complex_property_change() 66 | { 67 | var c = new Circle(); 68 | for (int i = 0; i < 10; i++) { 69 | c.Center = new Point(i + 1, i + 1); 70 | UndoCount(i + 1).RedoCount(0); 71 | } 72 | for (int i = 9; i >= 0; i--) { 73 | m.Undo(); 74 | Assert.AreEqual(new Point(i, i), c.Center); 75 | UndoCount(i).RedoCount(9 - i + 1); 76 | } 77 | for (int i = 0; i < 10; i++) { 78 | m.Redo(); 79 | Assert.AreEqual(new Point(i + 1, i + 1), c.Center); 80 | UndoCount(i + 1).RedoCount(9 - i); 81 | } 82 | } 83 | 84 | [TestMethod] 85 | public void Should_undo_multiple_properties_change() 86 | { 87 | var c = new Circle {Radius = 10, Center = new Point(10, 10)}; 88 | UndoCount(2); 89 | 90 | m.Undo(); 91 | Assert.AreEqual(new Point(0, 0), c.Center); 92 | UndoCount(1); 93 | 94 | m.Undo(); 95 | Assert.AreEqual(0, c.Radius); 96 | UndoCount(0); 97 | } 98 | 99 | [TestMethod] 100 | public void Should_reset_to_initial_states() 101 | { 102 | new Circle {Radius = 10, Center = new Point(10, 10)}; 103 | UndoCount(2); 104 | 105 | m.Reset(); 106 | UndoCount(0).RedoCount(0); 107 | } 108 | 109 | [TestMethod] 110 | public void Should_clear_redo_after_a_forward_change() 111 | { 112 | var c = new Circle {Radius = 10}; 113 | UndoCount(1).RedoCount(0); 114 | 115 | m.Undo(); 116 | UndoCount(0).RedoCount(1); 117 | 118 | c.Radius++; 119 | UndoCount(1).RedoCount(0); 120 | } 121 | 122 | [TestMethod] 123 | public void Should_be_able_to_piggy_back_undo_redo() 124 | { 125 | var c = new Circle {Radius = 10}; 126 | UndoCount(1).RedoCount(0); 127 | 128 | m.Undo(); 129 | Assert.AreEqual(0, c.Radius); 130 | UndoCount(0).RedoCount(1); 131 | 132 | m.Redo(); 133 | Assert.AreEqual(10, c.Radius); 134 | UndoCount(1).RedoCount(0); 135 | 136 | m.Undo(); 137 | Assert.AreEqual(0, c.Radius); 138 | UndoCount(0).RedoCount(1); 139 | 140 | m.Redo(); 141 | Assert.AreEqual(10, c.Radius); 142 | UndoCount(1).RedoCount(0); 143 | } 144 | 145 | [TestMethod] 146 | public void Should_undo_redo_whole_batch() 147 | { 148 | var circles = new Circle[10]; 149 | for (int i = 0; i < circles.Length; i++) { 150 | circles[i] = new Circle(); 151 | } 152 | 153 | m.Batch(() => { 154 | foreach (Circle circle in circles) { 155 | circle.Radius = 5; 156 | circle.Center = new Point(5, 5); 157 | } 158 | }); 159 | UndoCount(1); 160 | 161 | m.Undo(); 162 | foreach (Circle circle in circles) { 163 | Assert.AreEqual(0, circle.Radius); 164 | Assert.AreEqual(new Point(0, 0), circle.Center); 165 | } 166 | RedoCount(1); 167 | 168 | m.Redo(); 169 | foreach (Circle circle in circles) { 170 | Assert.AreEqual(5, circle.Radius); 171 | Assert.AreEqual(new Point(5, 5), circle.Center); 172 | } 173 | } 174 | 175 | [TestMethod] 176 | [ExpectedException(typeof (InvalidOperationException))] 177 | public void Should_throw_if_nesting_batches() 178 | { 179 | m.Batch(() => m.Batch(() => new Circle() {Radius = 5})); 180 | } 181 | 182 | [TestMethod] 183 | [ExpectedException(typeof (InvalidOperationException))] 184 | public void Should_throw_if_nesting_batches_via_explicit_calls() 185 | { 186 | m.BeginBatch(); 187 | m.BeginBatch(); 188 | m.EndBatch(); 189 | } 190 | 191 | [TestMethod] 192 | [ExpectedException(typeof(InvalidOperationException))] 193 | public void Should_throw_if_end_batch_without_starting_one() 194 | { 195 | m.EndBatch(); 196 | } 197 | 198 | [TestMethod] 199 | public void Should_not_throw_if_end_batch_after_starting_one() 200 | { 201 | for (var i = 0; i < 10; i++) { 202 | m.BeginBatch(); 203 | m.EndBatch(); 204 | } 205 | } 206 | 207 | [TestMethod] 208 | public void Should_track_based_on_enabling_setting() 209 | { 210 | m.IsTrackingEnabled = false; 211 | new Circle {Radius = 5}; 212 | UndoCount(0); 213 | 214 | m.IsTrackingEnabled = true; 215 | new Circle {Radius = 5}; 216 | UndoCount(1); 217 | } 218 | 219 | [TestMethod] 220 | public void Should_not_track_during_a_none_tracking_execution() 221 | { 222 | Assert.IsTrue(m.IsTrackingEnabled); 223 | m.ExecuteNoTrack(() => { 224 | Assert.IsFalse(m.IsTrackingEnabled); 225 | new Circle {Radius = 5, Center = new Point(5, 5)}; 226 | }); 227 | Assert.IsTrue(m.IsTrackingEnabled); 228 | UndoCount(0); 229 | } 230 | 231 | [TestMethod] 232 | public void Should_allow_nested_disabling_tracking() 233 | { 234 | Assert.IsTrue(m.IsTrackingEnabled); 235 | m.ExecuteNoTrack(() => { 236 | new Circle {Radius = 5, Center = new Point(5, 5)}; 237 | m.ExecuteNoTrack(() => { new Circle {Radius = 5, Center = new Point(5, 5)}; }); 238 | }); 239 | Assert.IsTrue(m.IsTrackingEnabled); 240 | UndoCount(0); 241 | } 242 | 243 | [TestMethod] 244 | public void Should_restore_to_previous_tracking_states_in_a_recursive_manner() 245 | { 246 | m.IsTrackingEnabled = false; 247 | m.ExecuteNoTrack(() => { 248 | m.IsTrackingEnabled = true; 249 | m.ExecuteNoTrack(() => { }); 250 | Assert.IsTrue(m.IsTrackingEnabled); 251 | }); 252 | Assert.IsFalse(m.IsTrackingEnabled); 253 | } 254 | 255 | [TestMethod] 256 | public void Should_allow_temporary_enabling_during_no_track_context() 257 | { 258 | m.ExecuteNoTrack(() => { 259 | var c = new Circle {Radius = 5}; 260 | m.IsTrackingEnabled = true; 261 | c.Radius++; 262 | m.IsTrackingEnabled = false; 263 | c.Radius++; 264 | }); 265 | UndoCount(1); 266 | } 267 | 268 | [TestMethod] 269 | public void Should_undo_redo_collection_addition() 270 | { 271 | var screen = new Screen(); 272 | var circle = new Circle(); 273 | screen.Add(circle); 274 | UndoCount(1); 275 | 276 | m.Undo(); 277 | Assert.AreEqual(0, screen.Shapes.Count); 278 | 279 | m.Redo(); 280 | Assert.AreSame(circle, screen.Shapes[0]); 281 | } 282 | 283 | [TestMethod] 284 | public void Should_undo_redo_collection_removal() 285 | { 286 | var screen = new Screen(); 287 | var circle = new Circle(); 288 | screen.Add(circle); 289 | m.Reset(); 290 | 291 | screen.Remove(circle); 292 | UndoCount(1); 293 | 294 | m.Undo(); 295 | Assert.AreSame(circle, screen.Shapes[0]); 296 | 297 | m.Redo(); 298 | Assert.AreEqual(0, screen.Shapes.Count); 299 | } 300 | 301 | [TestMethod] 302 | public void Should_undo_redo_collection_position_change() 303 | { 304 | var screen = new Screen(); 305 | Circle circle1, circle2; 306 | screen.Add(circle1 = new Circle()); 307 | screen.Add(circle2 = new Circle()); 308 | m.Reset(); 309 | 310 | screen.MoveToFront(1); 311 | Assert.AreSame(circle2, screen.Shapes[0]); 312 | Assert.AreSame(circle1, screen.Shapes[1]); 313 | 314 | m.Undo(); 315 | Assert.AreSame(circle1, screen.Shapes[0]); 316 | Assert.AreSame(circle2, screen.Shapes[1]); 317 | 318 | m.Redo(); 319 | Assert.AreSame(circle2, screen.Shapes[0]); 320 | Assert.AreSame(circle1, screen.Shapes[1]); 321 | } 322 | 323 | [TestMethod] 324 | [ExpectedException(typeof (ArgumentException))] 325 | public void Should_throw_when_removing_non_existent_element() 326 | { 327 | var screen = new Screen(); 328 | screen.Remove(new Circle()); 329 | } 330 | 331 | [TestMethod] 332 | [ExpectedException(typeof (ArgumentException))] 333 | public void Should_throw_when_changing_position_of_non_existent_element() 334 | { 335 | var screen = new Screen(); 336 | screen.Add(new Circle()); 337 | m.ElementIndexChange(screen.Shapes, new Circle()); 338 | } 339 | 340 | [TestMethod] 341 | public void Should_undo_redo_collection_changes_in_batch() 342 | { 343 | var screen = new Screen(); 344 | m.Batch(() => { 345 | var circle = new Circle(); 346 | screen.Add(new Circle {Radius = 10}); 347 | screen.Add(circle); 348 | screen.MoveToFront(1); 349 | screen.Remove(circle); 350 | }); 351 | Assert.AreEqual(1, screen.Shapes.Count); 352 | UndoCount(1); 353 | 354 | m.Undo(); 355 | Assert.AreEqual(0, screen.Shapes.Count); 356 | } 357 | 358 | [TestMethod] 359 | public void Should_undo_redo_collection_changes_in_explicit_batch() 360 | { 361 | var screen = new Screen(); 362 | m.BeginBatch(); 363 | try { 364 | var circle = new Circle(); 365 | screen.Add(new Circle {Radius = 10}); 366 | screen.Add(circle); 367 | screen.MoveToFront(1); 368 | screen.Remove(circle); 369 | } finally { 370 | m.EndBatch(); 371 | } 372 | Assert.AreEqual(1, screen.Shapes.Count); 373 | UndoCount(1); 374 | 375 | m.Undo(); 376 | Assert.AreEqual(0, screen.Shapes.Count); 377 | } 378 | 379 | [TestMethod] 380 | public void Should_fire_events() 381 | { 382 | int count = 0; 383 | m.Changed += (_, args) => count++; 384 | 385 | var circle = new Circle {Radius = 5}; 386 | Assert.AreEqual(1, count); 387 | 388 | circle.Center = new Point(5, 5); 389 | Assert.AreEqual(2, count); 390 | 391 | m.Batch(() => new Circle {Radius = 5, Center = new Point(5, 5)}); 392 | Assert.AreEqual(3, count); 393 | 394 | m.Undo(); 395 | Assert.AreEqual(4, count); 396 | 397 | m.Redo(); 398 | Assert.AreEqual(5, count); 399 | 400 | m.IsTrackingEnabled = false; 401 | new Circle {Radius = 5}; 402 | Assert.AreEqual(5, count); 403 | m.IsTrackingEnabled = true; 404 | 405 | m.ExecuteNoTrack(() => new Circle {Radius = 5, Center = new Point()}); 406 | Assert.AreEqual(5, count); 407 | 408 | m.Reset(); 409 | Assert.AreEqual(6, count); 410 | } 411 | 412 | [TestMethod] 413 | public void Should_fire_property_change_event() 414 | { 415 | new Circle {Radius = 10}; 416 | m.Changed += (_, args) => { 417 | Assert.AreEqual(typeof (PropertyChangeEvent), args.Event.GetType()); 418 | Assert.AreEqual(0, ((PropertyChangeEvent) args.Event).PropertyValue); 419 | }; 420 | m.Undo(); 421 | } 422 | 423 | [TestMethod] 424 | public void Should_fire_collection_addition_event() 425 | { 426 | var screen = new Screen(); 427 | var circle = new Circle(); 428 | 429 | int count = 0; 430 | m.Changed += (_, args) => { 431 | Assert.AreEqual(typeof(ElementAdditionEvent), args.Event.GetType()); 432 | Assert.AreSame(screen.Shapes, ((ElementAdditionEvent)args.Event).Collection); 433 | Assert.AreSame(circle, ((ElementAdditionEvent)args.Event).Element); 434 | count++; 435 | }; 436 | screen.Add(circle); 437 | m.Undo(); 438 | Assert.AreEqual(2, count); 439 | } 440 | 441 | [TestMethod] 442 | public void Should_fire_collection_removal_event() 443 | { 444 | var screen = new Screen(); 445 | var circle = new Circle(); 446 | screen.Add(circle); 447 | 448 | int count = 0; 449 | m.Changed += (_, args) => { 450 | Assert.AreEqual(typeof (ElementRemovalEvent), args.Event.GetType()); 451 | Assert.AreSame(screen.Shapes, ((ElementRemovalEvent)args.Event).Collection); 452 | Assert.AreSame(circle, ((ElementRemovalEvent)args.Event).Element); 453 | Assert.AreEqual(0, ((ElementRemovalEvent)args.Event).Index); 454 | count++; 455 | }; 456 | screen.Remove(circle); 457 | m.Undo(); 458 | Assert.AreEqual(2, count); 459 | } 460 | 461 | [TestMethod] 462 | public void Should_fire_collection_element_position_change_event() 463 | { 464 | var screen = new Screen(); 465 | var circle = new Circle(); 466 | screen.Add(new Circle()); 467 | screen.Add(circle); 468 | 469 | int count = 0; 470 | m.Changed += (_, args) => { 471 | Assert.AreEqual(typeof(ElementIndexChangeEvent), args.Event.GetType()); 472 | Assert.AreSame(screen.Shapes, ((ElementIndexChangeEvent)args.Event).Collection); 473 | Assert.AreSame(circle, ((ElementIndexChangeEvent)args.Event).Element); 474 | Assert.AreEqual(1, ((ElementIndexChangeEvent)args.Event).Index); 475 | count++; 476 | }; 477 | screen.MoveToFront(1); 478 | m.Undo(); 479 | Assert.AreEqual(2, count); 480 | } 481 | 482 | [TestMethod] 483 | public void Should_fire_batch_event() 484 | { 485 | int count = 0; 486 | m.Changed += (_, args) => { 487 | Assert.AreEqual(typeof (BatchEvent), args.Event.GetType()); 488 | Assert.AreEqual(2, ((BatchEvent) args.Event).Count); 489 | var events = ((BatchEvent) args.Event).ToArray(); 490 | Assert.AreEqual(typeof (PropertyChangeEvent), events[0].GetType()); 491 | Assert.AreEqual(typeof (PropertyChangeEvent), events[1].GetType()); 492 | count++; 493 | }; 494 | m.Batch(() => new Circle {Center = new Point(5, 5), Radius = 5}); 495 | m.Undo(); 496 | Assert.AreEqual(2, count); 497 | } 498 | 499 | [TestMethod] 500 | public void Should_handle_custom_event() 501 | { 502 | var reverseEvent = new CustomEvent(null); 503 | var @event = new CustomEvent(reverseEvent); 504 | m.MarkEvent(@event); 505 | UndoCount(1); 506 | 507 | m.Undo(); 508 | Assert.IsTrue(@event.IsRolledback); 509 | 510 | m.Changed += (_, args) => Assert.AreSame(reverseEvent, args.Event); 511 | m.Redo(); 512 | } 513 | 514 | [TestMethod] 515 | public void Should_throw_if_invalid_rollback_return_type() 516 | { 517 | m.Batch(() => { 518 | new Circle() {Radius = 10, Center = new Point(10, 10)}; 519 | }); 520 | BatchEvent batchEvent = null; 521 | MementorChanged changed = (_, args) => { 522 | batchEvent = (BatchEvent) args.Event; 523 | }; 524 | m.Changed += changed; 525 | m.Undo(); 526 | Assert.IsNotNull(batchEvent); 527 | 528 | m.Changed -= changed; 529 | var customEvent = new InvalidCustomEvent(batchEvent); 530 | m.MarkEvent(customEvent); 531 | 532 | try 533 | { 534 | m.Undo(); 535 | Assert.Fail("Expected InvalidOperationException"); 536 | } 537 | catch (InvalidOperationException) { 538 | } 539 | 540 | m.Reset(); 541 | m.Batch(() => { 542 | m.MarkEvent(customEvent); 543 | m.MarkEvent(customEvent); 544 | }); 545 | try 546 | { 547 | m.Undo(); 548 | Assert.Fail("Expected InvalidOperationException"); 549 | } 550 | catch (InvalidOperationException) 551 | { 552 | } 553 | } 554 | 555 | #region Helper 556 | 557 | private Features UndoCount(int c) 558 | { 559 | Assert.AreEqual(c, m.UndoCount); 560 | Assert.AreEqual(c > 0, m.CanUndo); 561 | return this; 562 | } 563 | 564 | private Features RedoCount(int c) 565 | { 566 | Assert.AreEqual(c, m.RedoCount); 567 | Assert.AreEqual(c > 0, m.CanRedo); 568 | return this; 569 | } 570 | 571 | #endregion 572 | } 573 | } 574 | 575 | // ReSharper restore InconsistentNaming --------------------------------------------------------------------------------