You are currently viewing a snapshot of www.mozilla.org taken on April 21, 2008. Most of this content is highly out of date (some pages haven't been updated since the project began in 1998) and exists for historical purposes only. If there are any pages on this archive site that you think should be added back to www.mozilla.org, please file a bug.



You are here: Editor project page > Transaction Manager

Transaction Manager

Kin Blas
Netscape Communications Corporation
Followup and discussion on netscape.public.mozilla.editor
Date last modified: Wed Feb 10 16:03:33 PST 1999

Overview

The next generation Mozilla editor uses a transaction system to manage all it's undoable/redoable actions. This system is made up of two parts, the set of editor defined transactions and the transaction manager, which is totally independent of the editor.

Although the transactions themselves are editor specific, the transaction interface (nsITransaction) they are based on isn't. This means that other applications like messenger, bookmarks, etc., can all create transactions for use with the transaction manager.

This document briefly describes how an application can use transactions and the transaction manager to create their own transaction system.

Transaction Manager Source

You'll find the source for the transaction manager in:


    mozilla/editor/txmgr

Despite the fact that the transaction manager code lives under the editor directory, I'd like to stress once more that it is totally independent of the editor.

Transaction Manager Features

Creating an instance of the Transaction Manager

Applications talk to the transaction manager through it's nsITransactionManager interface. This interface contains the methods for executing, undoing, and redoing transactions.

To create an instance of the transaction manager, the application must include the proper header files, and make sure that the transaction manager dll is registered with the nsRepository:


    #include "nsITransactionManager.h"
    #include "nsTransactionManagerCID.h"
    #include "nsRepository.h"


    static NS_DEFINE_IID(kISupportsIID, NS_ISUPPORTS_IID);
    static NS_DEFINE_IID(kITransactionManagerIID, NS_ITRANSACTIONMANAGER_IID);
    static NS_DEFINE_CID(kCTransactionManagerFactoryCID, 
    NS_TRANSACTION_MANAGER_FACTORY_CID);


    #ifdef XP_PC
    #define TRANSACTION_MANAGER_DLL "txmgr.dll"
    #else
    #ifdef XP_MAC
    #define TRANSACTION_MANAGER_DLL "TRANSACTION_MANAGER_DLL"
    #else // XP_UNIX
    #define TRANSACTION_MANAGER_DLL "libtxmgr.so"
    #endif
    #endif

    nsresult SomeMethod()
    {
      nsresult result;

      ...

      nsRepository::RegisterFactory(kCTransactionManagerFactoryCID,
                          TRANSACTION_MANAGER_DLL, PR_FALSE, PR_FALSE);

      ...

    }

It can then create an instance of the transaction manager by using the nsRepository::CreateInstance() method:


    nsresult result;
    nsITransactionManager *txmgr = 0;

    ...

    result = nsRepository::CreateInstance(kCTransactionManagerFactoryCID, nsnull,
                                   kITransactionManagerIID, (void **)&txmgr);

    if (NS_FAILED(result) || !txmgr) {
      return NS_ERROR_FAILURE;
    }

Executing a Transaction

To execute a transaction, you call the transaction manager's Do() method and pass in the transaction to execute:


    SomeTransaction *tx;

    /* Create a tx transaction here. */

    ...

    txmgr->Do(tx);

    ...

The Do(), Undo() and Redo() methods of a transaction should never be called directly by the application.

Undoing a Transaction

To undo the last transaction, you call the transaction manager's Undo() method:


    txmgr->Undo();

Redoing a Transaction

To redo the last transaction that was undone, you call the transaction manager's Redo() method:


    txmgr->Redo();

Multi Level Undo/Redo

The application can tell the transaction manager how many levels of undo/redo it wants by calling the SetMaxTransactionCount() method:

To specify 10 levels of undo, the applications would call:


    txmgr->SetMaxTransactionCount(10);

By default, the transaction manager is set to allow unlimited levels of undo/redo. Applications can also explicitly tell the transaction manager that this is what they want by passing in a -1:


    txmgr->SetMaxTransactionCount(-1);

Some applications may not want any levels of undo/redo, but still want the transaction manager to execute transactions, and not keep track of them for undo/redo purposes. This could be the case if the application is running under strict memory constraints. This can be set by passing a zero value:


    txmgr->SetMaxTransactionCount(0);

Coalescing of Transactions

One of the nsITransaction interface methods that all application transactions must implement is Merge(). The transaction manager uses the transaction at the top of the undo stack to call this method, to give it a chance to merge in a transaction whose Do() method was just executed. Here's an example to illustrate what happens inside the transaction manager.

Suppose the application executed transaction A:


    MyTransaction *A;
    MyTransaction *B;

    ...

    /* A and B get created somewhere here. */

    ...

    txmgr->Do(A);

and then executed transaction B:


    txmgr->Do(B);

Since A was at the top of the undo stack, executing B would result in the transaction manager calling A's Merge() method with B as it's argument:


    PRBool didMerge = PR_FALSE;

    A->Merge(B, &didMerge);

    if (didMerge) {
      /* nothing to do */
      return
    }
    else {
      /* wasn't merged so push B on the undo stack. */
    }

It's important to note that the transaction manager itself is not responsible for merging the transactions together. It is up to the application to merge B into A if it can! The transaction manager simply supplies the means to automatically call Merge() whenever a transaction is executed.

An example of how this feature may be used by an application, would be to merge separate transactions that inserted characters next to each other in the document, so that a single call to undo would undo them all at once.

Automatic Aggregation of Nested Transactions

Application transactions are allowed to execute other transactions from within their Do() methods. The transaction manager automatically keeps track of this relationship by aggregating the transactions together, so that one call to undo will undo all transactions.

In the following example, transactions A and B execute nested transactions:


    class PrintTransaciton : public nsITransaction {

      ...

      virtual void print() = 0;

      virtual nsresult Do()   { print(); }
      virtual nsresult Undo() { print(); }
      virtual nsresult Redo() { print(); }

      ...

    };


    class D : public PrintTransaction {

        ...

        void print() { printf("D\n");

        ...
    };



    class C : public PrintTransaction {

        ...

        void print() { printf("C\n");

        ...
    };



    class B : public PrintTransaction {

        ...

        void print() { printf("B\n");

        void Do()
        {
          C *c;

          /* Create c transaction here */

          ...

          PrintTransaction::Do();

          txmgr->Do(c);

          ...
        }

        ...
    };



    class A : public PrintTransaction {

        ...

        void print() { printf("A\n");

        void Do()
        {
          B *b;
          D *d;

          /* Create b and d transactions here */

          ...

          PrintTransaction::Do();

          txmgr->Do(b);
          txmgr->Do(d);

          ...
        }

        ...
    };

Executing an A transaction:


    A *a;

    /* Create an A transaction */

    ...

    txmgr->Do(a);

    ...

prints the following:


    A
    B
    C
    D

Calling Undo():


    txmgr->Undo();

prints the following:


    D
    C
    B
    A

Calling Redo():


    txmgr->Redo();

prints the following:


    A
    B
    C
    D

This feature is useful in applications like the editor, where most of the complex transactions that can be performed on a document, can be built from a few basic editing transactions like DeleteElement, InsertElement, and ChangeAttribute. Because these complex transactions are built entirely from basic transactions, they get undo and redo for free, because they are handled entirely by the basic transactions.

As an example, consider the case where the user types a key while some text is selected. The editor would execute a single transaction called InsertKey. All the InsertKey transaction does is iterate through the current selection, executing the DeleteItem transaction for every element it finds. Next, it executes an InsertElement transaction which inserts the character corresponding to the key that was typed.

Batching Transactions

There may be times when it is necessary to batch several independent transactions together so that a single call to undo, undoes them all at once. Applications can use the transaction manager methods BeginBatch() and EndBatch() to force transactions to be aggregated.


    SomeTransaction      *A;
    SomeOtherTransaction *B;

    /* Transactions created here */

    ...

    txmgr->BeginBatch();
    txmgr->Do(A);
    txmgr->Do(B);
    txmgr->EndBatch();

    ...

If we were to call txmgr->Undo() after executing this code, B would be undone, and then A. Executing a txmgr->Redo() after that would cause A to be redone, and then B.

Rollback for Error Recovery

The transaction manager assumes that transactions that throw an error during the execution of their Do(), Undo() or Redo() methods, leave the application in the state it was before execution was attempted.

In the case of a complex (aggregated) transaction, an error might occur somewhere after some of it's sub transactions might have already executed successfully. If this happens during an undo or redo, the transaction manager will attempt to rollback the transactions that previously succeeded to restore the application to it's previous state.

To illustrate this, suppose A was a complex transaction that executed transactions B, C, and D, like the example in the "Automatic Aggregation of Nested Transactions" section above.

Execution of txmgr->(A) would result in:


    A
    B
    C
    D

Now lets suppose that when we call txmgr->Undo(), B throws an error, this is what results:


    D   /* Undo */
    C   /* Undo */
    B   /* Undo error thrown */
    C   /* Rollback via Redo */
    D   /* Rollback via Redo */

Currently, there is no way to shut off this rollback behavior. It would be trivial to add a method to do this.

Listeners

The transaction manager sends out notifications whenever a transaction is done, undone, redone, or merged, and whenever a batch is started or closed. Applications or modules wishing to be notified of these events must implement the nsITransactionListener interface, and register themselves via the nsITransactionManager interface method AddListener().

Listeners can be used to generate transaction logs for debugging or QA test scripting. They can also be used to filter or forward transactions to some other handler.