Click here to Skip to main content
15,879,535 members
Articles / Desktop Programming / MFC
Article

PomDemo 1.0 - an object model construction demonstration

Rate me:
Please Sign up or sign in to vote.
4.29/5 (11 votes)
29 May 200332 min read 56.1K   839   40   5
A pattern for simplifying the building of applications supporting undo/redo, applications with complex dialogs and distributed applications

Sample Image - PomDemoWhole.jpg

Introduction

This is the first part of perhaps four articles describing an approach to building applications that greatly reduces the complexity of their implementation. Simultaneously the approach makes the creation of advanced features, like undo or distributed editing, almost trivial to implement.

The code was implemented as part of another personal project and is based on ideas developed having worked on a market leading commercial application.

This article presents PomDemo, a very trivial application allowing red blocks to be created, changed to green blocks and then deleted. The application nonetheless demonstrates some advanced features:

  • It contains a model for complexly constructed nested dialogs whose state can be modified and subsequently committed or discarded.
  • It implements infinite undo and redo of changes.
  • It allows the same data to be edited simultaneously within multiple instances of the application.

Future articles, or versions of this article, will expand the ideas presented here to do the following:

  • Improve the underlying change mechanism to work more optimally.
  • Allow the objects to be represented using other object technologies, specifically COM and .NET.
  • Extend the technology to work efficiently with arbitrarily large object models.

In this first version of PomDemo the framework pattern is implemented using two basic classes, together with a small family of derived classes, templates and macros. The two basic classes are CPomObject from which objects in the model are derived and CPomLayer which is used marshal access to the object model.

What a typical application wants

This is a brief look at what I believe an application typically wants to achieve.

When an application starts it will read or attach to a copy of a stored object model. This aspect of the application is neither approached nor changed by the framework described here.

When an application closes, or indeed at any point during its life, it will want to write or update the stored object model; while this may involve transferring the whole object model, ideally it would involve just writing those parts that have changed. An application may store its object model in a relational database and just want to update those rows that have changed. The ability to know what has changed, and specifically from what to what, is core to the presented framework.

Some actions have immediate effect on an object model. Deletions, while possibly guarded by an "are you sure" dialog, once instigated occur without further delay. Such actions can typically be reversed using an undo command.

Other actions have delayed effect and comprise a dialog that may require many user interactions. Upon completion such actions can be reversed using an undo command. More than that before completion the user may chose not to go ahead with the changes, perhaps pressing a Cancel button. While changes are being made but not committed it is often convenient to be able to place changes into the object model in a way that allows them to be easily removed, the ability to do this is also core to the presented framework.

The problems of multiple step user actions can be compounded when they also have multiple layers. A property sheet in which changes have been tentatively made may have a child dialog. This child dialog may need to know the changes made in the parent property sheet, but both changes made in the child dialog or in the property sheet need to be appropriately reversed should the user cancel one or both of them.

For many years now it has been a reasonable expectation on the parts of users that applications provide them with a means to undo their last few actions and subsequently redo them. This is similar to the other problems, a matter of producing a command object that can reverse the changes  to implement undo and reapply them to implement redo.

Not seen by the user, but in the mind of developers, is the need to reflect changes in the user interface. With a large object model it may not be practical to simply redraw everything on the screen. When information is held in controls, emptying and refilling them unnecessarily may lose state information that could surprise the user. Ideally only those parts of the user interface that have changed need to be redrawn or otherwise update.

The framework presented here also allows separate object models to be kept in synchronisation. This could be used to allow several users to simultaneously edit the same object model. In the personal project the framework is intended to be used in there is simultaneous access and update of the object model by both user and background tasks.

Trying to implement the above in isolation from one another can lead to unwieldy and complex code. Using MFC and Class Wizard in Developer Studio 6 to implement a dialog, for each attribute to be presented there would be a line of code in the caller to place the value into the dialog object and a line of code to extract it should the object's DoModal return IDOK. While this could be refined, complexity quickly starts arising and an increasing amount of the developers' time is spent writing housekeeping code. Using this framework to implement a dialog you need only pass a pointer to the object the dialog works on, no explicit housekeeping needs to be done by the developer.

What PomDemo does

PomDemo manipulates an object model entirely held in memory, that being perhaps the simplest manifestation of the animal. The object model in question only has two classes, CMyObject that stores information about a coloured block and CMyObjectCollection that holds the current list of these. This object model demonstrates objects being created, modified and destroyed.

Looking at the implementation of the objects, CMyObjectCollection really is simply a collection of CMyObjects and contains a single data member:

// List of objects.
CList<const CMyObject*, const CMyObject*> m_myObjectList;

The real currency of the application is CMyObject, another unusually simple class consisting of two data members, a screen co-ordinate for the block and a current state for the block:

// Position of item.
CPoint m_point;

// Current state.
enum STATE
{
	stateNew,
	stateChanged,
	stateDeleted
} m_state;

A block starts life as stateNew in which it is painted red on the screen, if we touch the object it changes to stateChanged and is painted green. Touching an object a second time deletes it, or at least marks it deleted using stateDeleted.

PomDemo in action

PomDemo is a simple MDI application without file support. Opening PomDemo will present you with an explorer style window containing a blank document. All user actions on the document result from either double clicking or right clicking in the right pane. Double clicking and right clicking produce exactly the same results, in the implementation double clicking is implemented through longhand code to show what is going on whereas the same operations are coded for right clicking using templates.

Double clicking in the right pane on the background will create a new red block. Double clicking a red block will turn it green. Double clicking a green block will delete it. This is only a demonstration so the application has no concept of selection or Z order.

When the first red block is created the Undo button on the toolbar becomes enabled. If the button is pressed the red block will be removed and the Redo button would become available.

The Undo button behaves exactly as a user would expect. If five changes are made, pressing Undo fives times reverses those changes.

The behaviour of the Redo button might give room for argument; it always attempts to redo the last undone action for that layer, even if you have made intervening changes. This is only a demonstration; a real application would probably discard the redo queue whenever a normal user action was made.

The layer tree

The left pane contains a tree control, initially with a single item in it labelled Root. New layers can be created using the Layer New menu item which produces the following dialog:

The name entered in the edit box has no significance other than it is displayed next to the layer in the tree.

In the version of PomDemo presented here the check box is never enabled. In future versions the check box is enabled when the parent layer is the key, identified with a . Non-key layers are marked . If the check box is checked then the parent item ceases to be the key and the new child inherits that role. The key layer is the one that contains the real object model.

When the selected layer in the tree is a leaf the Layer Delete menu item becomes available. Using the item will remove the selected layer from the tree. In future versions this is true even it is the key layer marked selected. It is normal for the real object model to be a member of the layer tree, but this is not a requirement of the framework.

Unless something is done that induces conflict, changes made to one layer appear in that layer and all of its descendent layers. If there are just two layers, the initial layer Root plus a created layer Child, all changes made to the right pane when Root is selected are duplicated in Child, however changes made to the right pane when Child is selected do not change Root. This is best observed by using Window New to open a second window. This effect is unchanged regardless of whether the key is Root, Child or neither.

Internal workings

With the exception of the key layer, layers store the changes between themselves and their parent. These changes can be made visible by using the toolbar button. When the button is depressed, rather than just displaying solid blocks the right pane presents the following objects:

Solid red block, an item created in this layer.
Solid green block, a changed item originally created in this layer.
Diagonally hatched red block, an unchanged item from a parent layer.
Diagonally hatched green block, a changed item from a parent layer.
Cross hatched green block, an unchanged item from a parent layer that has been changed in this layer.
Cross hatched black block, an item from a parent layer that has been deleted in this layer.
Cross hatched red block, a changed item from a parent layer that has become unchanged in this layer, an interesting possibility resulting from carefully sequenced use of Undo.

Conflict

It is possible to make changes to the object models presented in different layers that are incompatible with one another. In PomDemo it is clearly incompatible in one layer to change a block from red to green and in another layer to delete the same block. If this happens where the layers are ancestor and descendent a conflict results, in the framework this results in a conflict in the descendent layer.

The framework includes a mechanism for resolving conflicts, but where no resolution is possible it merely signals that there has been a conflict and reverses all the changes made in the conflicting layer.

In PomDemo conflicts in the collection of items are constantly occurring and being silently resolved. Conflicts in the items themselves are never resolved; they always result in the layer's changes being discarded. If a layer is being displayed when a conflict occurs it its background turns grey, a single left click on the background will clear this indicator.

Commit and Discard

When changes have been made to a layer other than the root two important toolbar buttons become available, Commit and Discard . Their functions are related, Commit makes all changes in the selected layer appear in its parent layer, Discard removes all the changes. Another way to view this is that Commit makes the parent layer the same as the child whereas Discard makes the child layer the same as the parent.

The layers within PomDemo can be viewed as modelling layers within a user interface, so Root could be the application's current object model, a child the object model as viewed from an open property sheet and a grandchild the object model as viewed from a dialog opened from the property sheet. Commit corresponds to the result of pressing the OK button on the corresponding user interface component while Discard represents pressing Cancel.

Synchronising object models

PomDemo can do something perhaps not seen very often. On the Layer menu the last two items are Share and Connect:

The Layer Share option, which is available for every layer, presents the following dialog:

The Port box refers to a TCP/IP port and selecting an available one will share the layer using that port of the host machine. Having shared a layer its icon will be overlaid with the usual Windows sharing hand, becoming either or .

Once shared the Share menu item will become Unshare, the selection of which will close the listening socket. Connections established through the share will still remain active.

The Connect item is only available when an empty layer is selected. When Connect is selected the following dialog appears:

The name or IP address of a machine sharing a layer is entered into the Server box together with the corresponding port in the Port box. When this is done successfully three things happen immediately. Firstly the name of the currently selected layer changes to have a one in parenthesis appended to it, perhaps , assuming no parenthesised number was already present. Secondly the corresponding layer on the server machine has a one in parenthesis appended, again assuming no parenthesised number was previously present. These numbers are merely a count of the connections active on the layer, they will be seen to increase and decrease as connections are made and broken.

The third thing that happens is that the previously empty layer is filled with whatever is in the corresponding layer of the server layer.

Once connected, changes made to one layer view of the object model are reflected in all connected layers. Thus two people can be creating and modifying blocks in PomDemo on two workstations simultaneously.

PomDemo allows connections to be made this way between any layers or machines, even layers within the same hierarchy. Clearly many possible topologies are meaningless or not useful, but likewise there are many useful ways of making connections.

A real application would have some handshaking to ensure that a change sent from one peer to another arrives and executes before another is sent, together with many other sanity checks. PomDemo is only a demo and carries no such code, it is possible to abuse the code such that it stops working.

The PomDemo source code

PomDemo does not do much editing, there is only one user action really to respond to, clicks. All of the work is really in the right hand pane's OnLButtonDblClk and OnRButtonDown methods.

In engineer Utopia we would perhaps just like to write the code to express what we want to do and nothing else. In the case of PomDemo this code might read something like the following:

void CMyDemoView::OnLButtonDblClk(UINT /* nFlags */ ,
		CPoint point)
{<BR>	// Get the collection.<BR>	CMyObjectCollection* const pMyObjectCollection = <BR>			GetDocument()->GetObjectCollection();<BR>
	// See if we've double clicked an existing object.<BR>	CMyObject* pMyObject = <BR>			pMyObjectCollection->HitTestObject(point);<BR>
<BR>	// If no object is found where the user double clicked...<BR>	if (pMyObject == NULL)<BR>	{<BR>		// ...we're creating a new object.<BR>
		// Create a new object.<BR>		CMyObject* const pMyObjectNew = new CMyObject(point);<BR>
		// Add the object to the collection.<BR>		pMyObjectCollection->Add(pMyObjectNew);<BR>	}<BR>	// Otherwise if the user clicked an existing item...<BR>	else<BR>	{<BR>		// ...we'll be making some change to it.<BR>
		// Cause it to change state.<BR>		ASSERT(!pMyObject->IsDeletedObject());<BR>		pMyObject->Touch();<BR><BR>		// Did that cause the object to disappear?<BR>		if (pMyObject->IsDeletedObject())<BR>		{<BR>			// Remove the deleted object from the collection.<BR>			pMyObjectCollection->Remove(pMyObject);<BR>
			// Free the object's storage.<BR>			delete pMyObject;<BR>		}<BR>	}
}

Unfortunately in the real world we are forced to do a little more work. The Utopian code neglects several important aspects of the PomDemo user interface, there are no hooks to update the display or code to handle the undo logic.

The OnLButtonDblClk

PomDemo allows editing to take place through both double clicking and right clicking. The double click code described here uses CPomLayer to make changes and includes some housekeeping code. The right click code makes exactly the same changes in the same way but, through the use of some templates, is exactly the same length as the Utopian code (it actually gains one statement at the top and loses the last statement at the end).

Looking at the code with the exposed housekeeping, CMyDemoView::OnLButtonDblClk starts with an extra line not in the Utopian code:

void CMyDemoView::OnLButtonDblClk(UINT /* nFlags */ ,<BR>		CPoint point)<BR>
{<BR>	// Get a new layer to make our change in.
	CPomLayer pomLayer(m_pMyLayer, false);

This creates a CPomLayer object, all changes to the object model are managed by layer objects, changes can only be added to leaf layers. Creating a new layer gives the context in which new changes can be made.

The function closes with the following:

	// Commit the changes to the model.
	pomLayer.Commit();
}

This causes all the changes made within function to become part of the layer above. Side effects of doing this include recording the changes for Undo in the parent layer (the layer visible in the right pane that was clicked on) and causing the changes to appear on the display.

In a template version a class CPomChangeLayer derived from CPomLayer is used. CPomChangeLayer maintains a stack of layers, the top of which is available through a static member function, and also automatically calls Commit in its destructor.

Reading the collection

In the Utopian version we just got the collection straight out the object model:

// Get the collection.
<BR>CMyObjectCollection* const pMyObjectCollectionRead = 
<BR>		GetDocument()->GetObjectCollection();

In the OnLButtonDblClk we need to obtain it through pomLayer:

// Get the collection as it is in our layer.
<BR>const CMyObjectCollection* const pMyObjectCollectionRead = 
		pomLayer.
		GetReadObject(GetDocument()->GetObjectCollection());

CPomLayer::GetReadObject will look for a locally changed version of its parameter and returns that if found. If no change is found then non-root non-key layers will call the same member of their parent. Finally if the function is called against the key layer the parameter is returned unchanged.

The right click version uses a template pointer that will obtain the read pointer from CPomLayer::GetReadObject when it is required.

Doing the hit test

The original code was as follows:

// See if we've double clicked an existing object.
<BR>CMyObject* pMyObject = 
<BR>		pMyObjectCollection->HitTestObject(point);

The OnLButtonDblClk code reads.

// See if we've double clicked an existing object.
<BR>const CMyObject* pMyObjectKey = 
		pMyObjectCollectionRead->HitTestObject(pomLayer, point);

The two important variables have had their names decorated with suffixes, the result is a pointer to a const object and pomLayer is passed to HitTestObject. Whereas the Utopian code has one pointer values through which it accesses data, CPomLayer identifies four values:

Key Pointer to the real object in the object model, or the object as it is originally allocated if it has not yet been incorporated into the object model. This pointer may be stored as a pointer to a const object so that its value is not inadvertently changed.
Read Pointer to the (const) object from which values should be read for a given layer. For most pointers in most layers this can be expected to be the same as the key pointer. Obtained using CPomLayer::GetReadObject and the key pointer.
Write Pointer to the (non-const) object that may be modified for a given layer. The pointer is obtained from CPomLayer::GetWriteObject using the key pointer, a new object will be allocated for the layer the first time it is called.
Change Pointer to the (const) object that corresponds to the changed value in the layer. Obtained from CPomLayer::GetChangeObject, it is very similar to that from CPomLayer::GetWriteObject except if no object has been allocated NULL is returned. A number of iterators are also provided for accessing these values.

The right click version only differs from the Utopian by using a template pointer.

Creating a new block

In Utopia we just create a new block thus:

// If no object is found where the user double clicked...
<BR>if (pMyObject == NULL)
<BR>	{
<BR>	// ...we're creating a new object.
<BR>
<BR>	// Create a new object.
<BR>	CMyObject* const pMyObjectNew = new CMyObject(point);

In OnLButtonDblClk we just have to do one extra step:

// If no object is found where the user double clicked...
if (pMyObjectKey == NULL)
{
	// ...we're creating a new object.

	// Create a new object.
	CMyObject* const pMyObjectNew = new CMyObject(point);
	pomLayer.CreateObject(pMyObjectNew);

The CreateObject call informs CPomLayer that it needs to manage the life of a new object. In the right click template version a CreateObject that returns its parameter is employed instead of the one in the base class.

PomDemo manually manages the life of objects, thus when an object is created by the framework to stand in for another it needs to be delete by the framework when it is no longer needed. CreateObject gives the framework the information it needs to do this.

Applications that use reference counting or garbage collection should not need to call CreateObject, objects can be created silently given that the framework sees changes to objects referring to the new arrivals.

Inserting the new member

The newly created object needs to be added to the list of objects, in the Utopia version this simply:

// Add the object to the collection.
<BR>pMyObjectCollection->Add(pMyObjectNew);

OnLButtonDblClk cannot simply do that because the pointers it has, the key version from CMyDemoDoc and the read version pMyObjectCollectionRead from GetReadObject are const and not intended for that purpose. Thus it needs to obtain a non-const write version and manipulate that:

// Get an editable collection.
CMyObjectCollection* const pMyObjectCollectionWrite = 
		pomLayer.
		GetWriteObject(GetDocument()->GetObjectCollection());

// Add the object to the collection.
pMyObjectCollectionWrite->Add(pMyObjectNew);

Doing that, together with the Commit line at the end of the function, completes the new object's creation. The template version hides the extra detail.

Changing the object

In Utopia we just changed the object by calling its Touch member.

// Cause it to change state.
<BR>ASSERT(!pMyObject->IsDeletedObject());
<BR>pMyObject->Touch();

Exactly as was the case with the collection object when we were creating a new object, the only pointer to the object OnLButtonDblClk has initially is const, so it needs to get a non-const version and touch that instead:

// Get an object editable object from the layer.
CMyObject* const pMyObjectWrite =
		pomLayer.GetWriteObject(pMyObjectKey);

// Cause it to change state.
ASSERT(!pMyObjectWrite->IsDeletedObject());
pMyObjectWrite->Touch();

Perhaps the layer should have been passed to Touch, but for the moment it does not deal with pointers to other objects and so it is completely safe to leave this detail out.

Handling deletion

Having been Touched an object may indicate that it does not want to go on living! In the Utopian version it was simply put out of its misery as follows.

// Did that cause the object to disappear?
<BR>if (pMyObject->IsDeletedObject())
{
<BR>	// Remove the deleted object from the collection.
<BR>	pMyObjectCollection->Remove(pMyObject);
<BR>
<BR>	// Free the object's storage.
<BR>	delete pMyObject;
}

IsDeletedObject is actually one of the CPomObject base class functions. (Having Object at the end of the name is a bit redundant, it was done to avoid conflicts where CPomObject was introduced as a new base to an existing class.) The OnLButtonDblClk code follows:

// Did that cause the object to disappear?
if (pMyObjectWrite->IsDeletedObject())
{
	// Get an editable collection.
	CMyObjectCollection* const pMyObjectCollectionLayer = 
			pomLayer.GetWriteObject(GetDocument()->
			GetObjectCollection());

	// Remove the deleted object from the collection.
	pMyObjectCollectionLayer->Remove(pMyObjectKey);
}

As before a writeable version of the collection needs to be obtained before the unwanted object's pointer can be removed from it. As always this extra step is avoided in the template using version.

In the Utopian code we had to delete the unwanted object. In OnLButtonDblClk the unwanted object is not quite unwanted, it is still needed in the parent layer and may be wanted in other layers. Thus CPomLayer takes over the responsibility for deleting the object, thus removing the delete line.

The CPomObject base class

In order for CPomLayer to work it requires the members of object models to be derived from CPomObject. The base class has an empty constructor and destructor and no data members. It defines five virtual function members, two of which are pure. Three member template functions (and three accompanying macros) are provided to help writing override implementations.

// Create an object based on this object.<BR>virtual CPomObject* CreateCopyObject() const = 0;

This is the first of the pair of pure virtuals. The function creates a clone of the original. CPomLayer uses this to create the objects returned the first time GetWriteObject is invoked for an object in that layer.

In a real application there is likely to be a derived class or two between CPomObject and the actual concrete classes. One of these classes might provide support for automatically cloning the object. Conceivably an MFC object could do this with little or no work. A new object of the same type could be created using CRuntimeClass::CreateObject and its data set using OverlayObject (below).

In a real application you might not actually want to exactly clone the whole object. CMyObjectCollection in PomDemo contains a list of objects which, if it were a real and useful application, could be extensive. A better solution might allow the copy to keep a pointer to the original and just record the differences made to it, in much the same way CPomLayer records changes to the model as a whole.

// Overlay content of this object with new content.<BR>virtual void OverlayObject(const CPomObject* const 
				pPomObjectMerge) = 0;<BR> 

The second required pure virtual in CPomObject. This is used to copy changes back to the original object when Commit is called.

For some limited objects implemented using MFC you could implement this by serializing out of one object into the other. Another method that might work in an MFC application is just memcpying one object over the other, this would work with simple types and pointers but would cause serious problems with CStrings, other more complex types and information carried in the class that is not part of the object model proper.

The remaining functions are not required, but in a real application better default implementations might be provided. 

// Create the merged product of this object with two of its 
// descendants, or NULL if such a product cannot be computed.
virtual CPomObject* CreateMergedObject(const CPomObject* const 
				pPomObjectSon,
		const CPomObject* const pPomObjectDaughter) const;

This is the only function of the seven that might be non-trivial to implement. The function merges two descendant objects back into their common ancestor. Except where one of the two descendants is found to be the same as the original ancestor the default implementation just gives up.

PomDemo CMyObjectCollection contains some fairly dumb code to merge its list of objects together:

  • If an object still appears in both descendant versions, it still appears in the merged version.
  • If an object has been removed from just one of the descendants, it does not appear in the merged version.
  • If an object has been added to just one of the descendant versions it appears in the merged version.
  • Otherwise, if a new object has been added to both changed versions or removed from both changed versions, CMyObjectCollection gives up and returns no object.

CMyObject does not override the function, mixing changes is always considered incompatible.

It might be argued that if two layers make the same change then there is a good case for believing that the change is one that should be made. In some applications this could be the case. In the general case it is not. Consider an object that contains an attribute containing a count of some other objects in the model. If two layers both increment the counter because they have added new countable objects, accepting the new value as the correct merged value is not valid, it should be N+2 not N+1.

// Test if object is marked as deleted.<BR>virtual bool IsDeletedObject() const; 

If an application inherently knows when the end of an objects useful life has come then this function does not need to be implemented. If an application needs to delete its objects manually, using delete in the absence of this framework perhaps, then rather than actually using delete an application should mark unwanted objects such that this function returns true. When the framework knows an object is no longer needed it will arrange for its deletion.

It is interesting to note that the framework actually has little interest in the creation and deletion of objects, indeed it has little interest in the object model outside of what is being changed. What interest it has really stems from its need to ensure there are no memory leaks and that it does not maintain versions of objects unnecessarily.

// Release the object, simply delete it or decrement its <BR>// reference count.<BR>virtual void ReleaseObject();

The framework does not simply delete objects as there may be subtleties to the way objects are disposed of. Specifically in a reference counting system deletion is implied rather than explicit. When an object is no longer needed this member is called in order to signal the framework has no further interest in it.

The default implementation of the function is delete this. Perhaps this function should have been made pure virtual.

CPomLayer

This article has already presented many of the features of CPomLayer. This section will pose and, hopefully, answer some questions about the class in order to hopefully flesh out more of its design and purpose.

Where is the object model?

The object model itself is really outside the scope of CPomLayer which really only concerns itself with changes within the model. Other than when there is change occurring within the object model CPomLayer maintains no pointers to it.

Why would you not have a key layer?

If you have a legacy application then you may have only partially adopted the scheme described here. Thus changes to the object model proper may be made through some other mechanism.

It is also possible that you never want to actually touch the object model, only to store the differences. Perhaps you have a game with a large and complex description of the environment and just want to track the player's interactions with it.

Why would you want the key layer anywhere other than the root?

An application may read a great deal of data from a database. The version of the object model from the database may be presented as the root layer. A child layer may then represent the object model as it has been modified by the user. Initially these will be identical and when the user saves their changes back to the database they will become identical once more.

When the user saves changes back to the database the only thing of real interest is the difference between what was stored and what needs to be stored, having the original object model available unedited is of no value.

If a user makes many changes to the object model and these are stored as changes to some base line, then it is possible that the process of substituting the edited version of an object for the real one might become very processor intensive. Thus having the most frequently referenced layer available as a plain text version of the object model may be important.

When are changes observed?

The application may only obtain writeable copies of objects from leaf layers. All other changes are made by calling Commit and Discard. As these functions are called virtual members are called within CPomLayer to inform derived classes of progress.

Of special interest here is OnChange, this gets called before a layer has new data merged into it. One parameter to OnChange contains the layer being merged, that is to say what this layer is going to become, while the other parameter is a Boolean indicating whether the change is the result of a Commit down from a child, as opposed to a change resulting from a Commit coming up from a parent. Where something is being kept in synchronisation with the object model, all changes need to be noted. Where user changes are being recorded in order that they may be replayed later in some way, then only those from Commit operations are of interest.

Two helper classes CPomObservedLayer and CPomObserver are used to distribute OnChange notifications as per the Observer Design Pattern. CPomObservedLayer is derived from CPomLayer and maintains a list of CPomObserver objects. When OnChange is called on CPomObservedLayer it relays the call onto all the CPomObserver objects in its list. In PomDemo CPomObserver is one of the base classes for the right pane view, thus it gets a kick whenever anything needs to be changed.

The actual concrete layer class used in PomDemo also overrides OnChange. Here it produces a CMyObjectCommand, part of the implementation of the Command Design Pattern. This is a computed object that contains a list of all the changed CMyObject instances. Just three possibilities exist, creation, modification and deletion. CMyObjectCommand actually stores a sequence of before and after pairs, the before value is not stored for creations, the after value is not stored for deletions.

When the change is due to a Commit the change is added to a list and is used to implement Undo.

If connections are present then the CMyObjectCommand object is transmitted to all the peers.

How are changes observed?

As indicated above changes are notified by passing round the before and after layers. In CPomLayer the before layer is itself, in CPomObserver it is passed explicitly as a parameter.

There are several ways of actually discovering changes between the layers. By brute force you could just iterate over both views of the object model and deduce the changes. CPomLayer does, however, provide more straightforward ways of finding the changes.

Like GetReadObject and GetWriteObject there is a third function GetChangeObject. Unlike the other two, which are guaranteed to return a pointer to an object of the same type as the input object, GetChangeObject only returns an object if it has changed between this layer and the previous. It is called on the after layer to detect changes to important objects, like the CMyObjectCollection instance in PomDemo.

It is also possible to enumerate through all the changes using GetStartChangePosition and GetNextChange, implemented here as iterators in the MFC style. Called on the after layer these enumerate what has changed. PomDemo uses these to find the objects that have been deleted.

A template CPomChangeIterator builds on the MFC style iterators to provide a more STL style of iteration over objects of the same type.

What goes on inside CPomLayer?

Often CPomLayer does not have to do much. Often there is a simple stack of layers, copies of objects to be made on the top of the stack are progressively overlaid onto the objects below. Reproducing such function is simple.

Sometimes CPomLayer has to do an awful amount of hard work. If the key layer is above the root and changes are committed to it, in order that the object model viewed from the root does not change compensating entries need to be made.

The initial version of CPomLayer accompanying this article has been reasonably well tested with non-reference counted object models where conflicts are avoided. You can use the simple block interface directly. If you click on the icon of the About dialog some extensive regression tests are run. There are likely to be bugs lurking in its handling of conflict situations and merging of non-root key layers, hence why some features were stripped from this version..

Open possibilities

The implementation of CPomLayer and CPomObject presented here are just enough to implement PomDemo and the personal project that required the code in the first place. The classes and the notions can be expanded in several directions, some of which are touched on below.

Reference counted and garbage collected versions

Much of the code presented here assumes objects are explicitly created and later explicitly deleted. Some small changes are needed to enable a paradigm where object lifetimes are dictated by reference counts or garbage collection. For reference counting the difference between the two paradigms lays in which event CPomLayer needs to be interested in, creation or deletion.

COM interface version

While the code here is all about pointers to C++ objects, changing the notion to apply to COM interfaces, or indeed CORBA or any other object technology, is not so difficult to imagine.

Threaded version

Except as already noted, when things become complex and changes are made between the root and key layer, no data in parent layers is ever touched until such time as Commit is called. This gives the opportunity to have threads that open their own layer and make wide ranging changes to their private view of an object model without needing to synchronise with other threads until one of them needs to Commit.

Combing version

There is a notion that, given the facility to Undo a mistake, a user will always reverse changes by using the Undo feature. This is not always true, a user inadvertently creating an unwanted object may simply delete it them self rather than pressing Undo. Similarly a user may make mistakes on purpose, using global search and replace to change 425 occurrences of "their" to "there" before changing back the three occurrences where they actually meant to write "their."

There may be situations where combing through the changes in before and after layers looking for non-changes may result in gains, if less time is spent spotting non-operations than might be spent executing them.

More communicative version

Currently there are only two virtual functions that can usefully be overridden from CPomLayer to track its changes in state. More virtual functions could be added to provide richer information as to what is going on. It may often be useful to maintain pointers to potentially changed, and hence moved, objects for extended periods of time. A virtual function that informed the application of changes of address could be deemed useful.

Optimised version

CPomLayer as presented and the extensions proposed for CPomLayer to support add more weight to the code and ultimately slow it down. Versions of CPomLayer that cut out support for unwanted features could be generated, as indeed could custom versions to fulfil requirements of specific projects.

Some of the implementation of CPomLayer is already non-optimal. There are objects created and immediately destroyed as part of the notification and change propagation mechanism.  Some more analysis for specific implementations could reduce this.

MFC free version

PomDemo and the personal project the code was written for are targeted at the Microsoft Windows platform and have been coded using MFC, not that MFC is particularly tied to that platform. The ideas contained can be freely translated to STL, .NET or any other frameworks or libraries.

Load object model on demand

All access to objects in the object model first pass through CPomLayer using a pointer to the subject object in the real object model. There is no reason why this pointer needs to be dereference other than within CPomLayer, so the pointer does not really need to be in the same address space as the current application, on indeed be a pointer at all. It could be arranged that an application is passed certain key pointers to objects within another address space. When the value behind a pointer is required CPomLayer could check to see if a local copy has been loaded, if not it could obtain a copy. In this way only those parts of an object model that were referred could be loaded.

Another mechanism could be in place to unload objects that are not currently required.

Where object models are synchronised between several processes, knowing what objects are current used in fellow processes could reduce unnecessary change notifications being sent.

Database update racing

There may be applications where you wish to maintain your object model in a database but not have to wait for database updates to complete.

This could be implemented as four CPomLayers. The bottom layer represents the database as it has been committed. The layer above that represents the changes that a thread is currently building a transaction to write to the database. The layer above that represents the outstanding changes that will be committed in the next database transaction. Finally the topmost layer of this stack represents the object model as seen by the application and which will periodically be committed to the CPomLayer controlling the database thread.

About the author

Pete has worked for several famous name companies including Microsoft, Channel Four Television and Orchestream. He has been responsible for the architecture and implementation of systems ranging between broadcast, word processing and life assurance quotations. Currently he working at Invensys helping to create the next generation of their market leading building automation systems, but would far rather be in Spain driving between Paradores.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralSilly me Pin
rpgage30-May-03 20:11
rpgage30-May-03 20:11 
GeneralRe: Silly me Pin
Chris Maunder31-May-03 3:15
cofounderChris Maunder31-May-03 3:15 
GeneralRe: Silly me Pin
Rob Manderson31-May-03 8:08
protectorRob Manderson31-May-03 8:08 
GeneralRe: Silly me Pin
peterchen1-Jun-03 16:32
peterchen1-Jun-03 16:32 
JokeRe: Silly me Pin
djionel8214-Sep-06 1:01
djionel8214-Sep-06 1:01 
me 2 .... PornDemo ... I think that we, developers, should not read such articles early in the morning or late in the afternoon ... Smile | :) ))

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.