├── .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 | [](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
--------------------------------------------------------------------------------