Entity Framework: Undo Redo v2

After my first Undo Redo POC version, one of my customers wanted to be able to manage many actions per Undo / Redo.

So I added two extension methods: BeginGroupOfUndoActions and EndGroupOfUndoActions.

My code is now this one:

public static class ObjectContextExtension

{

    private static Dictionary<ObjectContext, ObjectContextUndoRedo> _objectContextUndoRedo = new Dictionary<ObjectContext, ObjectContextUndoRedo>();

 

    public static void ActivateUndoRedoTracking(this ObjectContext context, int undoStackLength)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

        {

            objectContextUndoRedo = new ObjectContextUndoRedo { Context = context };

            _objectContextUndoRedo.Add(context, objectContextUndoRedo);

        }

        objectContextUndoRedo.ActivateUndoRedoTracking(undoStackLength);

    }

 

    public static bool CanUndo(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        return objectContextUndoRedo.CanUndo;

    }

 

    public static void Undo(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        objectContextUndoRedo.Undo();

    }

 

    public static bool CanRedo(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        return objectContextUndoRedo.CanRedo;

    }

 

    public static void Redo(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        objectContextUndoRedo.Redo();

    }

 

    public static void BeginGroupOfUndoActions(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        objectContextUndoRedo.MultipleActions = true;

    }

 

    public static void EndGroupOfUndoActions(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        objectContextUndoRedo.MultipleActions = false;

    }

 

    private class ObjectContextUndoRedo

    {

        private List<List<UndoRedoAction>> _undo, _redo;

        private int _undoStackLength;

        private bool _trackChanges;

 

        public ObjectContext Context { get; set; }

 

        private bool _multipleActions;

        public bool MultipleActions

        {

            get { return _multipleActions; }

            set

            {

                _multipleActions = value;

                if (value)

                    _undo.Insert(0, new List<UndoRedoAction>());

            }

        }

 

        public void ActivateUndoRedoTracking(int undoStackLength)

        {

            _undoStackLength = undoStackLength;

            _undo = new List<List<UndoRedoAction>>(undoStackLength);

            _redo = new List<List<UndoRedoAction>>(undoStackLength);

            _trackChanges = true;

 

            var objectStateEntries = Context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged).ToList();

 

            PropertyChangingEventHandler entityModifing = null;

            entityModifing = (sender, e) =>

            {

                var propInfo = sender.GetType().GetProperty(e.PropertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

                var value = propInfo.GetValue(sender, null);

                var undoRedoAction = new UndoRedoAction { EntityState = EntityState.Modified, UndoAction = () => propInfo.SetValue(sender, value, null) };

                var inpc = sender as INotifyPropertyChanged;

                if (inpc != null)

                {

                    PropertyChangedEventHandler entityModified = null;

                    entityModified = (s, e2) =>

                        {

                            if (e.PropertyName == e2.PropertyName && _trackChanges)

                            {

                                var newValue = propInfo.GetValue(sender, null);

                                undoRedoAction.RedoAction = () => propInfo.SetValue(sender, newValue, null);

                                if (MultipleActions)

                                    _undo[0].Add(undoRedoAction);

                                else

                                    _undo.Insert(0, new List<UndoRedoAction>() { undoRedoAction });

                                if (_undo.Count > _undoStackLength)

                                    _undo.RemoveAt(_undoStackLength);

                                _redo.Clear();

                            }

                            inpc.PropertyChanged -= entityModified;

                        };

                    inpc.PropertyChanged += entityModified;

                }

            };

            foreach (var e in objectStateEntries.Select(ose => ose.Entity as INotifyPropertyChanging).Where(inpc => inpc != null))

                e.PropertyChanging += entityModifing;

 

            Context.ObjectStateManager.ObjectStateManagerChanged += (sender, e) =>

            {

                switch (e.Action)

                {

                    case CollectionChangeAction.Add:

                        var inpc = e.Element as INotifyPropertyChanging;

                        if (inpc != null)

                            inpc.PropertyChanging += entityModifing;

                        break;

                }

            };

        }

 

        public bool CanUndo

        {

            get { return _undo != null && _undo.Any(); }

        }

 

        public void Undo()

        {

            if (!CanUndo)

                throw new InvalidOperationException();

            var undoRedoAction = _undo.First();

            _undo.RemoveAt(0);

            _trackChanges = false;

            foreach (var undoAction in undoRedoAction)

                undoAction.UndoAction();

            _trackChanges = true;

            _redo.Insert(0, undoRedoAction);

        }

 

        public bool CanRedo

        {

            get { return _redo != null && _redo.Any(); }

        }

 

        public void Redo()

        {

            if (!CanRedo)

                throw new InvalidOperationException();

            var undoRedoAction = _redo.First();

            _redo.RemoveAt(0);

            _trackChanges = false;

            foreach (var redoAction in undoRedoAction)

                redoAction.RedoAction();

            _trackChanges = true;

            _undo.Insert(0, undoRedoAction);

        }

    }

 

    private class UndoRedoAction

    {

        public EntityState EntityState { get; set; }

        public Action UndoAction { get; set; }

        public Action RedoAction { get; set; }

    }

}


And to make a demo about it, I did an unit test:


[TestClass]

public class ObjectContextExtensionTest

{

    [TestMethod]

    public void Test()

    {

        using (var context = new NorthwindEntities())

        {

            var c = context.Categories.First();

            context.ActivateUndoRedoTracking(5);

            var cOriginalCategoryName = c.CategoryName;

            c.CategoryName = “CN”;

            var c2 = context.Categories.OrderBy(c3 => c3.CategoryID).Skip(1).First();

            var c2OriginalCategoryName = c2.CategoryName;

            c2.CategoryName = “C2N”;

            c.CategoryName = “CN2″;

            context.Undo();

            Assert.AreEqual(“CN”, c.CategoryName);

            Assert.AreEqual(“C2N”, c2.CategoryName);

            context.Undo();

            Assert.AreEqual(“CN”, c.CategoryName);

            Assert.AreEqual(c2OriginalCategoryName, c2.CategoryName);

            context.Undo();

            Assert.AreEqual(cOriginalCategoryName, c.CategoryName);

            Assert.AreEqual(c2OriginalCategoryName, c2.CategoryName);

            context.Redo();

            Assert.AreEqual(“CN”, c.CategoryName);

            Assert.AreEqual(c2OriginalCategoryName, c2.CategoryName);

            context.Redo();

            Assert.AreEqual(“CN”, c.CategoryName);

            Assert.AreEqual(“C2N”, c2.CategoryName);

            context.Redo();

            Assert.AreEqual(“CN2″, c.CategoryName);

            Assert.AreEqual(“C2N”, c2.CategoryName);

 

            context.BeginGroupOfUndoActions();

            c.CategoryName = “CN3″;

            c2.CategoryName = “C2N2″;

            context.BeginGroupOfUndoActions();

            c.CategoryName = “CN4″;

            c2.CategoryName = “C2N3″;

            context.EndGroupOfUndoActions();

            c.CategoryName = “CN5″;

            c2.CategoryName = “C2N4″;

            context.Undo();

            Assert.AreEqual(“CN5″, c.CategoryName);

            Assert.AreEqual(“C2N3″, c2.CategoryName);

            context.Undo();

            Assert.AreEqual(“CN4″, c.CategoryName);

            Assert.AreEqual(“C2N3″, c2.CategoryName);

            context.Undo();

            Assert.AreEqual(“CN3″, c.CategoryName);

            Assert.AreEqual(“C2N2″, c2.CategoryName);

            context.Undo();

            Assert.AreEqual(“CN2″, c.CategoryName);

            Assert.AreEqual(“C2N”, c2.CategoryName);

            context.Redo();

            Assert.AreEqual(“CN3″, c.CategoryName);

            Assert.AreEqual(“C2N2″, c2.CategoryName);

            context.Redo();

            Assert.AreEqual(“CN4″, c.CategoryName);

            Assert.AreEqual(“C2N3″, c2.CategoryName);

            context.Redo();

            Assert.AreEqual(“CN5″, c.CategoryName);

            Assert.AreEqual(“C2N3″, c2.CategoryName);

            context.Redo();

            Assert.AreEqual(“CN5″, c.CategoryName);

            Assert.AreEqual(“C2N4″, c2.CategoryName);

        }

    }

}

 

 

This entry was posted in 7671, 7674. Bookmark the permalink.

10 Responses to Entity Framework: Undo Redo v2

  1. Vikas says:

    Fro where download this framework

  2. Matthieu MEZIL says:

    nowhere. All the code is here.

  3. Ivan says:

    This doesn’t work. I tested it in my application and doesn’t work.

  4. wiktor256 says:

    Great code. I use it to undo all changes when the user clicks on a “Cancel” button.

    I think I stumbled on a small bug though. The undo list becomes empty after doing group undo. Then making a change to an entity causes an IndexOfOutOfBounds exception to be thrown in the line below.

    if (MultipleActions)
    _undo[0].Add(undoRedoAction);

  5. Matthieu MEZIL says:

    It’s just a POC guys.
    So it’s possible that there is some bugs. Sorry for it but I am too busy to fix them.
    Matthieu

  6. wiktor256 says:

    I see that this approach works for undo/redo of simple property changes. Am I missing something?

    Is there a way to also undo/redo changes in relationships between entities? For example, when a new Order is added to the Customer->Orders collection?

  7. Matthieu MEZIL says:

    In RelatedEnd class, you have an AssociationChanged event so I think that you can use it to extend my code.

  8. wiktor256 says:

    Hi,
    I’ve been trying to get it to work, and I am getting closer, but it’s not pretty. I have to resort to using reflection to access unexposed .NET methods and properties. I know, that’s bad, but I want to see if it is possible to make it work. The main reason for using the reflection is that there doesn’t seem to be a way to undelete an entity. After poking through .NET code, I found that ObjectStateEntry.RevertDelete() does the trick.

  9. pogo says:

    Did you sucessfully make it work ? How ?
    Thanks.

  10. Fred says:

    Hey wiktor256

    Did you manage to get the Undo/Redo work with relationships ?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>