Merging Transactions

Coordinator
Feb 23, 2010 at 9:16 PM

Mark D has written:


Thanks for the code. It's very well written and has saved me a lot of time starting from scratch. I'm in the process of converting this code to actionscript and using it in a Flex application I'm working on.  My application is a kind of rich text editor.  Things are going well, but I'm starting to think that I'm using transactions in a way that you may not have intended to, and I was wondering if you could give me your opinion on it.

In my case, when you type a character, one of several actions can occur depending on different things.  For instance, inserting a text node with a value, or if the current node is currently a text node, adding a character to it and lastly moving the caret position to after the character.

The way I implemented this is that I made actions for adding nodes, removing nodes, inserting characters into existing text nodes, and lastly moving the caret.

I made use of the transaction and created a transaction at the highest level in my code as possible, basically in the keypress handler.  That way I didn't have to concern myself with all the different permutations the code took when handling keypress.  All I cared about was at the end that if you undid the action, it would undo everything that was created during that keypress.

This works fine, however, it totally disables the "merging" aspect of the framework.  It seems that the multiAction used in transactions will never attempt to merge.  The tryToMerge method returns false.

I can kind of see why you did it this way since the "rules" for merging multiactions may be specific to the application.

I'm considering altering this portion of the code to somehow iterate through the children in some systematic way to determine if things can be merged, but I have quite figured out how I'm going to do this.

One way is to just look at the last guy in the multiaction, but that won't help me since for me that's usually a move caret action.  I need to look one more back into the multiaction to find the "insert text node" or the "insert character into a text node" actions.  In this case subsequent insert characters should merge with this.  This starts to get complicated though so I wonder if I'm looking at this the wrong way.

If you have any insight into this, I would be interested to hear it.

Thanks.


Coordinator
Feb 23, 2010 at 9:28 PM

Mark,

this is a very interesting point. I don't have a single answer for you but I do have several points for you to choose from.

First, take a look at the TextBox implementation in my StructuredEditor project: http://structurededitor.codeplex.com/SourceControl/changeset/view/31583#459735

I chose to not store caret movements in the Undo buffer, so caret position changes aren't recorded. However I do see that it might be useful to undo caret movements and being able to merge edits. In this case, here's what I would probably do: Define a higher-level InsertText action that inherits from Transaction and create that explicitly, instead of creating an implicit transaction in your key event handler. Override TryToMerge on it and define your custom logic to get the previous InsertText, and combine the two texts and the two caret positions into one. You will likely need to encapsulate insert text in a high-level entity anyway, because you might later want to show strings like "Undo inserting (2) characters". You will be able to reuse this action for Paste as well.

Finally, take a look at the latest source code of the Undo Framework - I've simplified things a little bit, hopefully this can improve the situation.

As for AllowToMergeWithPrevious, users can manually disable this in their code when they record the action, and the action might process it in TryToMerge (if(!AllowToMergeWithPrevious) return false;). You're right however, it's not used anywhere as of now.

Do let me know if you have any questions,

Kirill

Feb 25, 2010 at 12:51 AM

Thanks for the explanation.  Here's what I wound up doing, though it's still a work in progress.

What I did was make a single action for inserting characters.  That action essentially is called when the user types a character.  It pretty much winds up calling my high level "InsertCharacterAtPosition" method.  The unExecute for that just calls my existing high level "DeleteCharacterFromPosition" method.  During the execute and unExecute, I store off the caret position since I need to replace it prior to calling my high level routines again on undo/redo.  This worked well, and I was able to make the merge work since it wasn't inside a transaction.

What happened next was kind of interesting.  I decided to work on the "DeleteCharacter".  This one was tricky.  There are many paths the code takes during this operation and at different levels in my object structure since my editor is made up of paragraphs and paragraphs have items and so forth.  Depending on where the caret is, when you delete a character you can be deleting entire items, characters within an text item, or even merging items or paragraphs.  Therefore undoing this action is complicated since you would need to know what you "did" initially.  Keeping track of that is annoying.  So fOr this case I used a transaction and made actions for lower level things like, inserting an item, or creating an item.  The problem is that I also needed to call actionManager.recordaction inside the code that my InsertCharacter action was using since it was inserting items sometimes as well.  So what I did was in my InsertCharacter action, I made sure I set the flag to "ExecuteImmediatelyWithoutRecording".  That way, that action could call the other actions, but they would not get recorded. (I had to adjust a line of code in your framework to get this to work).  However, when I called them via my "DeleteCharacter" action which was in a transaction, they would get recorded.  This is looking like it's working well, and the code is pretty manageable.

The one side effect I noticed is that when I delete characters, since they are being done inside a transaction, I can't merge so each undo is a single character in the case of simple typing and deleting.  However, I have an idea on how to fix that.  Basically, when a MultiAction is "recorded", I want to see if it has only one action in it.  This is the case when I'm deleting simple character from the text item.  If a MultiAction has only one action in it, it should able to be replaced by the action itself.  I'm attempting to handle this in SimpleHistory.AppendAction.  The only problem I have at the moment is that I'm using isDelay=false since my sequence of "mini Actions" require things to be a certain way  while executing from one to another.  The IsDelay is stored in the MultiAction and if I replace it with a regular action, the regular action will get executed a second time after the append.  I'm working on a solution now.

BTW,  you mention getting the latest code.  I have the code from http://undo.codeplex.com/.  It's dated july 09.  Is that the latest.