EF : Undo Redo

How to use Undo/Redo with EntityFramework? This isn’t managed by EF.


So we will have to do it ourselves.


We could clone the entities and group them into a Stack but in this case I worry about my memory usage growing too much.


So I prefer another solution with Action.


Note that for this POC, I just manage Undo/Redo on scalar properties.


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;

        if (_objectContextUndoRedo.ContainsKey(context))

            objectContextUndoRedo = _objectContextUndoRedo[context];

        else

        {

            objectContextUndoRedo = new ObjectContextUndoRedo { Context = context };

            _objectContextUndoRedo.Add(context, objectContextUndoRedo);

        }

        objectContextUndoRedo.ActivateUndoRedoTracking(undoStackLength);

    }

 

    public static bool CanUndo(this ObjectContext context)

    {

        if (!_objectContextUndoRedo.ContainsKey(context))

            throw new InvalidOperationException();

        return _objectContextUndoRedo[context].CanUndo;

    }

 

    public static void Undo(this ObjectContext context)

    {

        if (!_objectContextUndoRedo.ContainsKey(context))

            throw new InvalidOperationException();

        _objectContextUndoRedo[context].Undo();

    }

 

    public static bool CanRedo(this ObjectContext context)

    {

        if (!_objectContextUndoRedo.ContainsKey(context))

            throw new InvalidOperationException();

        return _objectContextUndoRedo[context].CanRedo;

    }

 

    public static void Redo(this ObjectContext context)

    {

        if (!_objectContextUndoRedo.ContainsKey(context))

            throw new InvalidOperationException();

        _objectContextUndoRedo[context].Redo();

    }

 

    private class ObjectContextUndoRedo

    {

        private List<UndoRedoAction> _undo, _redo;

        private int _undoStackLength;

        private bool _trackChanges;

 

        public ObjectContext Context { get; set; }

 

        public void ActivateUndoRedoTracking(int undoStackLength)

        {

            _undoStackLength = undoStackLength;

            _undo = new List<UndoRedoAction>(undoStackLength);

            _redo = new 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);

                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);

                                _undo.Insert(0, 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;

            undoRedoAction.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;

            undoRedoAction.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; }

    }

}


 The main question in my opinion is: does a complete unod/redo make sense? Indeed, we will have some big issues with cascade, Identity, concurrent access…

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

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>