N-Level Undo

Posts   
 
    
njetty
User
Posts: 12
Joined: 22-Nov-2007
# Posted on: 22-Nov-2007 22:34:34   

I've implemented a partial working solution to the "N-Level Undo" problem using posts found on this site. I still have a problem with "New" entities added to child collections and how to remove them. The code snippet billow was added to the "CommonEntityBase" so it can be generic to any entity. Please help. See below....

    public void DeepUndo()
    {
        ObjectGraphUtils utils = new ObjectGraphUtils();
        List<IEntity> list = utils.ProduceTopologyOrderedList(this);
        foreach (IEntity entity in list)
        {
            if (entity.IsNew)
            {
                // How to delete entity from graph here ????
            }
            else if (entity.IsDirty)
                entity.Refetch();

        }
    }
Walaa avatar
Walaa
Support Team
Posts: 14993
Joined: 21-Aug-2005
# Posted on: 23-Nov-2007 10:03:55   

IMHO Deep Undo should be implemented in BL, with a utility method that tackes an entity as an entry point of a graph, and then traverse its related entities and collections, that way you can set null for a related new entity, and you can remove a new entity from the container Collection.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39859
Joined: 17-Aug-2003
# Posted on: 23-Nov-2007 11:23:45   

The way you propose it, it's not going to work. I've been puzzling about this for a long time and it at first seems possible, but then it appears to become pretty tricky and you run into problems where you have to reset reference other entities etc. So I never found a way to do this. For v3 I have to implement this in both the designer and the runtime, so I had to find a solution for this. To illustrate why we need this: in v3 we'll have an object model in memory and various editors on it (text, UI). You can make changes with a simple command like checking a checkbox or writing a line of DSL code. This can have deep consequences like making it impossible to have a typedlist T because it can't exist anymore in that form because you changed something in an entity, e.g. hide a relation the typedlist was build on. So to be able to make this possible, a change to the object model, the graph, should be able to be rolled back if the changes following the change run into a problem that they're impossible. So before a change should be made, a transaction should be started, the change should be made, and if it runs into a problem, the transaction should roll back, otherwise the transaction should succeed.

This is what I've dubbed 'transactional graph operations'. I'll first refer to a paper about efficient state management in a graph using transactional logic. It's a scientific paper, so it might appear a little daunting and complex if you're not used to read these, but that will go away once you grasp the concept. simple_smile http://arxiv.org/abs/0709.1699

Now, that's fairly complex and it can help making things very efficient. The root issue here is though that you keep track of state changes in every place where a state can change. So, the idea is that you follow these steps in a program: - first start a transaction on the graph - every element / node in the graph gets notified, it is part of the transaction - when a change occurs on one (or more, that's not important) node, it logs its changes. - for every change it knows how to roll that particular change back. So setting a field to a value: reverse it with the old value. Setting a reference to an entity: reset it with the previous value. Adding an entity to a collection: remove it from the collection. - when the update of the elements is complete, the transaction is committed. This means: all nodes in the graph at THAT point are notified that they're in the committed state. This means that they have to throw away their tmp data, no roll backs are possible after this state. All entities which are no longer in the graph are known by the transaction itself. The transaction has to clean these up so they're not tied to elements into the graph - when the update of the elements is complete and the transaction fails due to an error, the transaction rolls back. This is done by reversing the commands on the graph in the reverse order in which they appeared.

In general this is the command pattern, where every command is logged in a queue and you can move back/forth in that queue. Every command's action on a node can be reversed by the node, similar to that each node can perform the action of the command.

The tricky part is that when you add a node to the graph at some point, and you want to roll back the graph's changes, you have to roll back that addition as well.

This isn't easy. In a lot of cases, e.g. edit screens which modify an in-memory graph and the user clicks cancel, it's often cheaper to simply refetch the graph.

Frans Bouma | Lead developer LLBLGen Pro
njetty
User
Posts: 12
Joined: 22-Nov-2007
# Posted on: 23-Nov-2007 16:42:58   

It's funny you mentioned refreshing the graph. This is what I ended up doing. I guess something told me the approach I was hoping to implement would not be possible. Since the "refetch()" method per entity is not a "Deep Refetch" I was forced to do a "GetMulti()" on the parent entities container collection. Unfortunately I am unable to support "Undo" per parent entity within the app. The Undo I have given users will instead revert all activity across multiple parent entities.

rdhatch
User
Posts: 198
Joined: 03-Nov-2007
# Posted on: 17-Jan-2009 17:01:14   

Hi Frans -

Any updates on this? I need OK/Cancel functionality (without refetching from database).

Right now I'm doing this: * user selects entity & hits edit button * clone entity (using serialize/deserialize) * pass cloned entity to edit form * if user hits cancel - throw away cloned entity * if user hits ok - need to overwrite original entity with cloned entity - but HOW do I do this?

You had mentioned in a thread that the Context could be used.

Frans wrote: You could serialize it indeed, all state required is serialized as well. What's better: the ObjectID's (to identify an instance) are also serialized so you can eventually use these to find back an entity.

You could use a Context to update the entities after your popup screen closes. First clone, then assign the original entities to a context, then open your popup, do the editing etc. when the edit is done, you simply do a Get of the entities changed from the context, and the fields of the original instance are updated automatically.

Can this be achieved using the Context? And if so - how?

Any help you could provide would be greatly appreciated! Thanks!

Ryan D. Hatch

Other thread: http://llblgen.com/tinyforum/Messages.aspx?ThreadID=7551

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39859
Joined: 17-Aug-2003
# Posted on: 18-Jan-2009 10:21:47   

Undo/redo is something you either have to build in in every small little element of an object graph or you have to use copy-edit-restore.

As the current code doesn't have undo/redo build in in every small little element, you have to use the - create clone - edit - cancel? -> throw away edited version - OK? -> save edited version, throw away copy.

approach

The context isn't something you can use to get older versions back, you have to use SaveFields(name) to save an entity's field values, and use RollbackFields(name) to roll back the state. that's for single entities. If you update/edit relations as well (e.g. you add a new order to a customer's Orders collection), you can't undo that with SaveFields obviously, and this is precisely the tough issue with undo/redo and object graphs: references. Here you need a copy of the graph or complex code which keeps track of deletes/updates of references.

For v3's designer, which will have undo/redo everywhere, I've written a complex layer for this, based on .NET 3.5 and lambdas. It works transparent for the programmer, so you can do whatever you want with the object graph and undo/redo is build in. The thing is that using it inside the runtime lib will cause a fork in the code (as the undo/redo is based on .NET 3.5), and building it in takes time (every property has to be undo/redo aware, which is done using a special generic member object), so it's not likely we'll add this feature directly into the runtime at launch of v3.0. However, the library will be open source for v3 licensees, so you can build undo/redo aware object graphs with it for the other platforms we support in v3.

For now however, stick with the approach I described above.

Frans Bouma | Lead developer LLBLGen Pro