Oblivion Mod: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.
Contents
Basic Update Systems[edit]
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[edit]
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
- CObMultiRecordHandler
- 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[edit]
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 < 0)return //Actually save the record data GetControlData() //Perform any additional updates OnPostSaveRecord() //Close or update depending on operation if (Saving) Close() else SetControlData() endif
The OnPreSaveRecord() method is needed to prepare the record for an update. For example, the record may need to be moved to the active file, any change to its editorid needs to be checked, etc.... The CObRecordDialog::OnPreSaveRecord() looks like:
//Update the editorid from the UI (do not save to record), check for validity Result = UpdateEditorID() if (Result < 0) return (Result) Result = m_pDlgHandler->OnPreSaveRecord(EditInfo) if (Result < 0) return (Result)
The EditInfo variable holds information about the record being edited. The CObEditDialogHandler::OnPreSaveRecord() looks like.
Result = m_pRecordHandler->OnPreSaveCheckEditorID(EditInfo) if (Result < 0) return (Result) //Prompt for a rename/copy if needed if (Result == OBE_RESULT_PROMPTRENAMECOPY) { Result = PromptRenameCopy(EditInfo) if (Result < 0) return (Result) } Result = m_pRecordHandler->OnPreSaveRecord(EditInfo) if (Result < 0) return (Result)
At this point no changes have yet to have been saved to the record but it is ready to have any changes applied to it. When editing records these changes are applied in the CObRecordDialog::GetControlData() method or in one of its virtual methods. After this the CObRecordDialog::OnPostSaveRecord() method is called:
//Just call the dialog handler event m_pDlgHandler->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[edit]
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[edit]
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[edit]
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[edit]
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[edit]
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[edit]
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[edit]
Based on the above information a design for the event/listener system can be built.
Event[edit]
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[edit]
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[edit]
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 };