Tes4Mod:ObEdit/Event Processing

An issue encountered with MwEdit was that the process of updating the user interface from changes in the plugin was a complex, and somewhat fragile, process. Even early in the development of ObEdit this problem can be seen to repeat itself. A better designed system for UI updates is needed.

Basic Update Systems
For a simple application updating the user interface is typically a simple process. In an application such as ObEdit, however, the data structure and interface design is much more complex and a 'simple update' is not possible or practical. There are several basic types of update systems that can be considered:

:*Dumb Updates -- Such a system just redraws everything when an update is needed. This doesn't work well for ObEdit as 'everything' gets far too large very quickly. A typically session might be displaying a record list, dozens of open records, a cell view, find dialogs, and multiple other windows. A dumb system would force the updating of everything each time any change is made to the data. This would result in performance and usability issues.
:*Specific Direct Updates -- This is the update system that was used in MwEdit and initially used in ObEdit. It attempts to update only that which may have changed. For example a record addition only needs to update a single item in the record list while a record deletion would also need to update or close any open windows using that record. The updates are direct as the UI components are called directly with the update information. This makes it difficult, or impossible, or perform updates from the lower levels of data where there is no direct connection to the interface component. Such updates need to be moved farther up in the class hierarchy. This works well for a simple or moderately complex application but was found to be difficult to manage well in MwEdit.
:*Event/Listener Updates -- In this system the interface and data components can be completely separated and connected only through an event messaging system. The data manipulation layers post update events which are caught and handled by any registered listeners. Event messages can be direct calls that are handled instantly, queued to be handled in groups at a later time, or a combination of both. This is the type of update system to be used in ObEdit.


Edit Process
Before the event/listener system is designed or implemented we need to consider the basic design of the ObEdit application including the needed flow of information and the editing process. The event/listener system will have to easily accommodate all edit processes as well as be flexible enough to support additional processes that may be used in the future.

The basic class hierarchy of ObEdit is:
: CObEditView
:: CObRecordListCtrl
:: CObRecordListTree
:: CObEditDocument
::: CObMultiRecordHandler
:::: CObEspFile
:: CObEditDialogHandler
::: CObRecordDialog

The CObMultiRecordHandler class handles most of the data manipulation while CObEditView handles the majority of the user interface updates. CObMultiRecordHandler cannot 'know' anything about the user interface system as it is a common library component that will be used in multiple projects using different UIs. The event/listener system will deal with indirectly connecting CObMultiRecordHandler to CObEditView and the other UI components.

Edit Record
This will be a very common operation and has a relatively complex process. All record dialogs will be derived from the CObRecordDialog class which will handle much of the necessary update procedures. Records are not actually updated until the save or apply button is pressed. This makes the cancel operation simply closing the dialog. A save operation is the same as an apply except the dialog is closed after a successful save while the dialog stays open on an apply.

Note that possible locations of event signals are enclosed in [brackets].

The save/apply events in CObRecordDialog basically look like:

        //Prepare the record for saving
    Result = OnPreSaveRecord
    if (Result  OnPreSaveRecord(EditInfo)
    if (Result  OnPreSaveCheckEditorID(EditInfo)
    if (Result  OnPreSaveRecord(EditInfo)
    if (Result  OnPostSaveRecord()

The CObEditDialogHandler::OnPostSaveRecord() looks like:

    m_pRecordHandler->OnPostSaveRecord(EditInfo)
    
    if (EditInfo.IsNew || EditInfo.IsCopy)
        [Send Add Record Event]
    else
        [Send Update Record Event]
    endif

Clean Record
Cleaning a record removes it from the active file and is much simpler process:

    [ Send Close Record Event ]
    pNewRecord = m_pRecordHandler->CleanRecord(pRecord)
    [ Send Clean Record Event ]

Note that the close record event might be combined with the clean event, as long as any open windows that are using the existing record are closed.

Add Record
Adding a record is virtually identical to editing a record. A temporary record is created and used for editing. If the edit is cancelled the temporary record is merely deleted. On a save/apply the temporary record is made permanent, moving to the active file. The remaining process is identical as editing a record. This is all performed by the CObMultiRecordHandler::OnPreSaveRecord() method.

Undo Action
The undo action is essentially performing the opposite of a command. Undoing an add record action is a delete operation. Undoing a clean record action is adding or updating a record. Undoing an update action is either an update or clean operation.


UI Updates
There are a variety of UI components that need to be updated when the mod data changes:

:* Record list 
:* Record tree (if counts are enabled)
:* Undo list (if undo is enabled)
:* All open record dialogs
:* Any other open window


Update Types
There are only three basic types of updates:

:* Add record (add new or create copy of existing)
:* Clean record (replaced with non-active version)
:* Clean record (delete)
:* Update record (from non-active version)
:* Update record (from active version)


Update Information
A certain amount of information is required for each type of update:

:; Add :
::* Record that was added

:; Clean:
::* Record that was cleaned
::* Previous version of the record (if any)

:; Update:
::* Record that was updated
::* Previous version of the record (if any)


Event/Listener Design
Based on the above information a design for the event/listener system can be built.


Event
An event simply holds information about a particular update:

    class CObEvent {
        int          m_Type         //Type of event, add/update/clean
        CObRecord*   m_pOldRecord   //Previous record version, if any
        CObRecord*   m_pNewRecord   //Current record version, if any
    };


Listener
There are various ways a listener class can be implemented but an interface design will be used in our case:

   class IObListener {
   public:
       virtual int OnListenAddRecord   (CObEvent& Event) = 0;
       virtual int OnListenCleanRecord (CObEvent& Event) = 0;
       virtual int OnListenRecord      (CObEvent& Event) = 0;
    
       virtual int GetListenEvents (void) = 0;   //Which events the listener wants
   };

Classes that need to respond to update events will simply derive from the IObListener interface and implement the abstract methods as needed.

Event Handler
The event handler is the center of the event/listener system. It receives events and notifies the appropriate listeners.

    class CObEventHandler {
    protected:
        IObListener  m_Listeners[];
        CObEvent     m_QueuedEvents[];
    
    public:
    
        void Add    (IObListener* pListener);
        void Remove (IObListener* pListener);
    
        void SendEvent  (CObEvent& Event);    //Send an event to all listeners immediately
        void QueueEvent (CObEvent& Event);    //Add an event to send later
    
        void CombineQueuedEvents (void);      //Combine any similar queued events
        void SendQueuedEvents    (void);      //Send all queued events
    
    };